Jens Roeser

  • Associate Professor in Psycholinguistics, Nottingham Institute of Psychology, Nottingham Trent University
  • Research: language production, comprehension, and acquisition
  • Focus: real-time written language production
  • Methods: Bayesian modelling, keystroke logging, eye tracking
  • Teaching: (advanced) inferential statistics, data processing, data visualisation / dashboards
  • Contact: jens.roeser@ntu.ac.uk
Nottingham, UK

Why Preprocess Data?

Most time spent working with data is not spent running statistical tests.

It is spent turning raw data into evidence we can trust.

Preprocessing matters because it:

  • checks whether the data match what happened in the study
  • documents decisions about exclusions, recoding, and missing values
  • protects raw data from manual spreadsheet edits
  • makes the workflow reproducible with open-source tools
  • creates analysis-ready data for statistics, figures, and sharing

Overview

ProjectKnow where files live
ReadCSV, Excel, SPSS
InspectCheck structure using e.g.  glimpse
Process select, filter, mutate
SummariseUse summarise + .by

Throughout, I will first introduce functions which we then practice in exercises.

Download The Workshop Folder From GitHub

Run one line in R:

source(url("https://tinyurl.com/lplus-dataprocessing-ws"))

This creates:

  • lplus-2026-dataprocessing-ws/

Then open:

  • lplus-2026-dataprocessing-ws/slides.html
  • lplus-2026-dataprocessing-ws/lplus-2026-dataprocessing-ws.Rproj

RStudio Projects

An RStudio Project is a folder-based workspace.

What it does:

  • marks the workshop folder as one self-contained project
  • opens RStudio in the correct working directory

Why it matters:

  • paths start from the project folder
  • scripts, data, slides, and exercises stay together

For this workshop, open the provided R project file.

Why Projects Matter

With the project open, these paths work:

read_csv("data/chinese_ldt.csv")

Without the project open, R may look in the wrong folder.

Check your current project folder:

getwd()

Example:

"/home/jensroes/lplus-2026-dataprocessing-ws"

Creating A Project

For your own work later:

  • File > New Project
  • choose New Directory or Existing Directory
  • keep raw data in a data/ folder
  • keep scripts in a scripts/ or exercises/ folder
  • open the .Rproj file before working

Project paths should be relative:

read_csv("data/my_data.csv")

Installing And Loading Packages

install.packages(c("tidyverse", "readxl", "haven"))

Install packages once.

library(tidyverse)
library(readxl)
library(haven)

Load packages in every new R session where you want to use them.

What Is The Tidyverse?

The tidyverse is a collection of R packages for working with data.

  • readr reads rectangular data files
  • readxl reads Excel files
  • dplyr transforms and summarises data
  • tidyr changes data shape
  • ggplot2 makes plots

The packages share a common style: data first, instructions second.

Tidyverse And Base R

Goal Base R Tidyverse
read CSV data read.csv("file.csv") read_csv("file.csv")
select variables data[, c("ppt_id", "rt")] select(data, ppt_id, rt)
filter rows data[data$rt > 2000, ] filter(data, rt > 2000)
create a variable data$rt_sec <- data$rt / 1000 mutate(data, rt_sec = rt / 1000)
summarise mean(data$rt) summarise(data, mean_rt = mean(rt))

The tidyverse versions use consistent function names and avoid repeatedly typing the data frame name inside each instruction.

One Shared Pattern

Most tidyverse data processing functions follow one logic.

verb(data, instructions)

Examples:

select(data, variable)
filter(data, variable > 100)
mutate(data, new_variable = old_variable * 2)
summarise(data, mean_variable = mean(variable))

Chinese Lexical Decision Task

Participants saw stimulus images in Chinese or Latin script and decided whether each one was a real word.

Script Word Nonword
Chinese Chinese real word stimulus cat / word Chinese nonword stimulus cat / nonword
Latin Latin real word stimulus cat / word Latin nonword stimulus cat / nonword

Each row in the dataset is one response trial from the online task export.

More Variables

The data also contain useful predictors and item information.

Task variables:

  • ppt_id, trial, stimulus, script, lexicality
  • word_eng, response, correct_response

Participant variables:

  • age, bilingual_status

Outcome variables:

  • rt, rt_sec, correct

Reading CSV Files

chinese_ldt_base <- read.csv("data/chinese_ldt.csv")
class(chinese_ldt_base)
[1] "data.frame"
# "data.frame"

Base R returns a data frame. read_csv returns a tibble.

chinese_ldt <- read_csv("data/chinese_ldt.csv")
class(chinese_ldt)
[1] "spec_tbl_df" "tbl_df"      "tbl"         "data.frame" 
# "spec_tbl_df" "tbl_df" "tbl" "data.frame"

Reading Excel Files

library(readxl)

chinese_ldt_excel <- read_excel("data/chinese_ldt.xlsx")

Use this when collaborators send .xlsx files.

Reading SPSS Files

library(haven)

chinese_ldt_spss <- read_sav("data/chinese_ldt.sav")

Use this when data come from SPSS.

RStudio Import Dataset

For spreadsheet-style importing:

  • Use Import Dataset
  • Choose the file
  • Check the preview
  • Copy the generated code into your script

The non-code route is useful for discovery, but the generated code should still end up in the script.

Inspect Before Changing

glimpse(chinese_ldt)
names(chinese_ldt)
count(chinese_ldt, lexicality)

Open exercises/01_reading_inspecting.R.

Selecting Variables

Keep only the variables you need.

select(chinese_ldt, ppt_id, stimulus, rt)
# A tibble: 3,360 × 3
  ppt_id stimulus                       rt
   <dbl> <chr>                       <dbl>
1      1 latin_realword_scoop.png     1420
2      1 chinese_nonword_grass.png    2955
3      1 chinese_realword_shave.png    817
4      1 latin_realword_home.png      1571
5      1 chinese_nonword_sand.png     1036
6      1 chinese_realword_change.png   903
# ℹ 3,354 more rows

Selecting Many Variables

select(chinese_ldt, ppt_id, stimulus, contains("response"))
# A tibble: 3,360 × 4
  ppt_id stimulus                    correct_response response
   <dbl> <chr>                       <chr>            <chr>   
1      1 latin_realword_scoop.png    word             word    
2      1 chinese_nonword_grass.png   nonword          word    
3      1 chinese_realword_shave.png  word             word    
4      1 latin_realword_home.png     word             word    
5      1 chinese_nonword_sand.png    nonword          nonword 
6      1 chinese_realword_change.png word             word    
# ℹ 3,354 more rows

Dropping Variables

Use - inside select to remove variables.

select(chinese_ldt, -word_eng, -rt_sec)
# A tibble: 3,360 × 11
  ppt_id trial stimulus               script lexicality   age bilingual_status
   <dbl> <dbl> <chr>                  <chr>  <chr>      <dbl> <chr>           
1      1     1 latin_realword_scoop.… latin  word          22 balanced        
2      1     2 chinese_nonword_grass… chine… nonword       22 balanced        
3      1     3 chinese_realword_shav… chine… word          22 balanced        
4      1     4 latin_realword_home.p… latin  word          22 balanced        
5      1     5 chinese_nonword_sand.… chine… nonword       22 balanced        
6      1     6 chinese_realword_chan… chine… word          22 balanced        
# ℹ 3,354 more rows
# ℹ 4 more variables: correct_response <chr>, response <chr>, correct <lgl>,
#   rt <dbl>

Store The Result

Tidyverse functions return a new data frame.

select(chinese_ldt, ppt_id, stimulus, rt)

This shows the result, but does not save it.

chinese_ldt_small <- select(chinese_ldt, ppt_id, stimulus, rt)

This saves the result so the next line can use chinese_ldt_small.

Filtering Rows

Keep rows that match a condition.

filter(chinese_ldt, lexicality == "word")
# A tibble: 1,680 × 13
  ppt_id trial stimulus      script lexicality word_eng   age bilingual_status
   <dbl> <dbl> <chr>         <chr>  <chr>      <chr>    <dbl> <chr>           
1      1     1 latin_realwo… latin  word       scoop       22 balanced        
2      1     3 chinese_real… chine… word       shave       22 balanced        
3      1     4 latin_realwo… latin  word       home        22 balanced        
4      1     6 chinese_real… chine… word       change      22 balanced        
5      1     7 chinese_real… chine… word       bag         22 balanced        
6      1     9 chinese_real… chine… word       peach       22 balanced        
# ℹ 1,674 more rows
# ℹ 5 more variables: correct_response <chr>, response <chr>, correct <lgl>,
#   rt <dbl>, rt_sec <dbl>

Filtering Rows

Keep rows that match a condition.

filter(chinese_ldt, rt > 2000)

Filtering With Multiple Conditions

filter(chinese_ldt, rt > 2000, lexicality == "word")
filter(chinese_ldt, lexicality %in% c("word", "nonword"))

Missing Values

Some other example data:

# A tibble: 20,293 × 3
     ID   Age Education     
  <int> <int> <fct>         
1 51624    34 High School   
2 51625     4 <NA>          
3 51626    16 <NA>          
4 51627    10 <NA>          
5 51628    60 High School   
6 51629    26 9 - 11th Grade
# ℹ 20,287 more rows

Missing values are NA, not "NA".

Missing Values

filter(chinese_ldt, is.na(rt)) # find missing rt values
# A tibble: 0 × 13
# ℹ 13 variables: ppt_id <dbl>, trial <dbl>, stimulus <chr>, script <chr>,
#   lexicality <chr>, word_eng <chr>, age <dbl>, bilingual_status <chr>,
#   correct_response <chr>, response <chr>, correct <lgl>, rt <dbl>,
#   rt_sec <dbl>
filter(chinese_ldt, !is.na(rt)) # remove missing rt values

or

drop_na(chinese_ldt, rt)

Creating Variables

mutate adds or changes columns.

mutate(
  chinese_ldt,
  rt_sec = rt / 1000
)

More Mutate Examples

mutate(
  chinese_ldt,
  correct = response == correct_response,
  slow_trial = rt > mean(rt)
)

Open exercises/02_select_filter_mutate.R.

Descriptive Summaries

summarise(
  chinese_ldt,
  mean_rt = mean(rt),
  sd_rt = sd(rt),
  accuracy = mean(correct),
  n = n()
)
# A tibble: 1 × 4
  mean_rt sd_rt accuracy     n
    <dbl> <dbl>    <dbl> <int>
1   2399. 4980.    0.888  3360

Summaries By Group

Use .by for grouped summaries.

summarise(
  chinese_ldt,
  mean_rt = mean(rt),
  sd_rt = sd(rt),
  accuracy = mean(correct),
  n = n(),
  .by = lexicality
)
# A tibble: 2 × 5
  lexicality mean_rt sd_rt accuracy     n
  <chr>        <dbl> <dbl>    <dbl> <int>
1 word         2301. 5047.    0.904  1680
2 nonword      2496. 4912.    0.873  1680

Two Grouping Variables

summarise(
  chinese_ldt,
  mean_rt = mean(rt),
  accuracy = mean(correct),
  n = n(),
  .by = c(script, lexicality)
)
# A tibble: 4 × 5
  script  lexicality mean_rt accuracy     n
  <chr>   <chr>        <dbl>    <dbl> <int>
1 latin   word         2223.    0.904   840
2 chinese nonword      2436.    0.862   840
3 chinese word         2380.    0.905   840
4 latin   nonword      2556.    0.883   840

Open exercises/03_summarise_by.R.

Integrated Exercise

Build a short preprocessing workflow:

  1. Read with read_csv
  2. Inspect with glimpse
  3. Reduce with select and filter
  4. Create with mutate
  5. Summarise with .by

Open exercises/04_integrated_live_exercise.R.

Recommended Reading

Follow-Up Practice: A New Dataset

Function Use it for
read_csv / read_excel / read_sav read common file formats into R
glimpse inspect variable names and types before editing
select keep or drop variables to make data more manageable
filter remove errors, practice trials, or implausible responses
mutate create or change variables needed for analysis
summarise + .by turn trial-level data into grouped descriptive results
  • Apply the same workflow to data/blomkvist.csv.
  • Follow-up script: exercises/06_followup_blomkvist.R.
  • A useful next step is pipe-based preprocessing workflows: |> or %>%.

Tidy Data And Pivoting

Messy summary:

ppt_id word_rt nonword_rt
1 812 940

Tidy summary:

ppt_id lexicality mean_rt
1 word 812
1 nonword 940

Tidy data makes the next verb easier to write.

Pivoting For Reshaping Data Frames

chinese_ldt_summary <- summarise(
  chinese_ldt,
  mean_rt = mean(rt),
  .by = c(ppt_id, lexicality)
)
# A tibble: 168 × 3
  ppt_id lexicality mean_rt
   <dbl> <chr>        <dbl>
1      1 word         1167.
2      1 nonword      1479.
3      2 nonword      1333.
4      2 word         1582.
5      3 word         1692.
6      3 nonword      1429.
# ℹ 162 more rows

Pivot Wider

Create one row per participant, with separate columns for words and nonwords.

chinese_ldt_summary_wide <- pivot_wider(
  chinese_ldt_summary,
  names_from = lexicality,
  values_from = mean_rt
)
# A tibble: 84 × 3
  ppt_id  word nonword
   <dbl> <dbl>   <dbl>
1      1 1167.   1479.
2      2 1582.   1333.
3      3 1692.   1429.
4      4 1124.   2159.
5      5  728.   1299.
6      6 1388.   1015.
# ℹ 78 more rows

Pivot Longer

pivot_longer(
  chinese_ldt_summary_wide,
  cols = c(word, nonword),
  names_to = "lexicality",
  values_to = "mean_rt"
)
# A tibble: 168 × 3
  ppt_id lexicality mean_rt
   <dbl> <chr>        <dbl>
1      1 word         1167.
2      1 nonword      1479.
3      2 word         1582.
4      2 nonword      1333.
5      3 word         1692.
6      3 nonword      1429.
# ℹ 162 more rows

Pivoting exercise

exercises/05_optional_pivoting_backup.R.