Jens Roeser

  • Associate Professor in Psycholinguistics, Nottingham Institute of Psychology, Nottingham Trent University
  • Focus: real-time planning in 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. Everything will be made available.

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 the provided R project file for this workshop:

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

RStudio Projects: .Rproj

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

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 r/ folder (today we use exercises/)
  • open the .Rproj file before working

Project paths should be relative:

read_csv("data/my_data.csv")

Installing And Loading Packages

install.packages("tidyverse")

Install packages once. Installing tidyverse also installs readxl and haven.

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

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

What Is The Tidyverse?

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

  • readr reads table-like text files, such as CSV files
  • readxl reads Excel files
  • haven reads SPSS, Stata, and SAS files
  • dplyr transforms and summarises data
  • tidyr changes data shape
  • ggplot2 makes plots

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

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

Tidyverse And Base R

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

Goal
Base R
Tidyverse
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))

Chinese Lexical Decision Task

Participants saw stimulus images in Chinese or Latin script and decided whether each one was a real word. Example item for cat:

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

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

Note: The original export was far more messy than what we will work with.

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, language_status

Outcome variables:

  • rt, rt_sec, correct

Reading CSV Files

chinese_ldt <- read_csv("data/chinese_ldt.csv")

read_csv returns a tibble.

class(chinese_ldt)
[1] "spec_tbl_df" "tbl_df"      "tbl"         "data.frame" 

Base R returns a data frame.

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

Why Use Tibbles?

Tibbles are modern data frames designed for interactive work.

They are useful because they:

  • print compactly instead of filling the console
  • show variable types such as <chr>, <dbl>, and <lgl>
  • make it easier to notice when column names changed during import
  • avoid surprise changes such as automatically turning text into factors
  • work smoothly with tidyverse verbs such as select, filter, mutate, and summarise

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.

Three Core Data Processing Tools

Many tidyverse functions are useful, but three verbs do much of the everyday work:

  • select chooses columns
  • filter chooses rows
  • mutate creates or changes columns

Together, these three functions let you make a raw data frame smaller, cleaner, and more useful for analysis.

Selecting Variables

Keep only the variables you need.

select(chinese_ldt, ppt_id, stimulus, rt)
# A tibble: 3,640 × 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,634 more rows

Selecting Many Variables

select(chinese_ldt, ppt_id, stimulus, contains("response"))
# A tibble: 3,640 × 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,634 more rows

Dropping Variables

Use - inside select to remove variables.

select(chinese_ldt, -word_eng, -rt_sec, -stimulus, -language_status)
# A tibble: 3,640 × 9
  ppt_id trial script lexicality   age correct_response response correct    rt
   <dbl> <dbl> <chr>  <chr>      <dbl> <chr>            <chr>    <lgl>   <dbl>
1      1     1 latin  word          22 word             word     TRUE     1420
2      1     2 chine… nonword       22 nonword          word     FALSE    2955
3      1     3 chine… word          22 word             word     TRUE      817
4      1     4 latin  word          22 word             word     TRUE     1571
5      1     5 chine… nonword       22 nonword          nonword  TRUE     1036
6      1     6 chine… word          22 word             word     TRUE      903
# ℹ 3,634 more rows

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") # notice the double equals!
# A tibble: 1,820 × 5
  ppt_id trial script  lexicality    rt
   <dbl> <dbl> <chr>   <chr>      <dbl>
1      1     1 latin   word        1420
2      1     3 chinese word         817
3      1     4 latin   word        1571
4      1     6 chinese word         903
5      1     7 chinese word        1270
6      1     9 chinese word         784
# ℹ 1,814 more rows

Filtering Rows

Keep rows that don’t match a condition.

filter(chinese_ldt, lexicality != "word") # notice the exclamation point
# A tibble: 1,820 × 5
  ppt_id trial script  lexicality    rt
   <dbl> <dbl> <chr>   <chr>      <dbl>
1      1     2 chinese nonword     2955
2      1     5 chinese nonword     1036
3      1     8 latin   nonword     2506
4      1    11 chinese nonword      835
5      1    14 latin   nonword      886
6      1    15 latin   nonword     1788
# ℹ 1,814 more rows

Filtering Rows

Keep rows that match a condition.

filter(chinese_ldt, rt > 2000) # `>` means "larger than"
# A tibble: 752 × 5
  ppt_id trial script  lexicality    rt
   <dbl> <dbl> <chr>   <chr>      <dbl>
1      1     2 chinese nonword     2955
2      1     8 latin   nonword     2506
3      1    18 chinese nonword     2339
4      1    21 latin   word        2721
5      1    24 latin   nonword     4042
6      1    40 chinese word        4008
# ℹ 746 more rows

Filtering With Multiple Conditions

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

Missing Values

Some participants have no coded language_status.

# A tibble: 5 × 3
  ppt_id   age language_status
   <dbl> <dbl> <chr>          
1      1    22 balanced       
2      4    24 <NA>           
3     43    30 <NA>           
4     45    29 <NA>           
5     50    20 balanced       

Missing values are NA, not "NA".

Missing Values

filter(chinese_ldt, is.na(language_status)) # find missing values
filter(chinese_ldt, !is.na(language_status)) # remove missing values

or

drop_na(chinese_ldt, language_status)

Creating Variables

mutate adds or changes columns.

mutate(
  chinese_ldt,
  rt_sec = rt / 1000
)
# A tibble: 3,640 × 6
  ppt_id trial script  lexicality    rt rt_sec
   <dbl> <dbl> <chr>   <chr>      <dbl>  <dbl>
1      1     1 latin   word        1420  1.42 
2      1     2 chinese nonword     2955  2.96 
3      1     3 chinese word         817  0.817
4      1     4 latin   word        1571  1.57 
5      1     5 chinese nonword     1036  1.04 
6      1     6 chinese word         903  0.903
# ℹ 3,634 more rows

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   2469. 5069.    0.865  3640

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         2365. 5031.    0.879  1820
2 nonword      2573. 5107.    0.851  1820

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         2305.    0.874   910
2 chinese nonword      2507.    0.846   910
3 chinese word         2426.    0.884   910
4 latin   nonword      2639.    0.855   910

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

Wide summary:

# A tibble: 1 × 3
  ppt_id word_rt nonword_rt
   <dbl>   <dbl>      <dbl>
1      1     812        940

Tidy summary:

# A tibble: 2 × 3
  ppt_id lexicality    rt
   <dbl> <chr>      <dbl>
1      1 word         812
2      1 nonword      940

When conditions are stored in one column, we can use the same tidyverse verbs across conditions instead of writing separate code for each condition.

Pivoting For Reshaping Data Frames

chinese_ldt_summary <- summarise(
  chinese_ldt,
  mean_rt = mean(rt),
  .by = c(ppt_id, lexicality)
)
# A tibble: 182 × 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.
# ℹ 176 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: 91 × 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.
# ℹ 85 more rows

Pivot Longer

pivot_longer(
  chinese_ldt_summary_wide,
  cols = c(word, nonword),
  names_to = "lexicality",
  values_to = "mean_rt"
)
# A tibble: 182 × 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.
# ℹ 176 more rows

Pivoting exercise

exercises/05_optional_pivoting_backup.R.