library(tidyverse)
library(janitor)
library(afex)
library(emmeans)
library(caret)Data Skills for Psychology — Spring 2026
Ten weeks of data skills — let’s see how far we’ve come.
A tour through every module this quarter:
We’ll use the Faul et al. (2025) emotional memory dataset throughout.
Every R program is built from five things:
| Concept | What it does | Example |
|---|---|---|
| Variables | Name a value | n_participants <- 307 |
| Vectors | Store multiple values | c(19, 23, 45, 67) |
| Loops | Repeat code | for (s in subjects) { ... } |
| Branches | Make decisions | if (age > 35) { ... } |
| Functions | Package reusable code | my_fun <- function(x) { ... } |
Warning
Mixing types in a vector causes coercion — everything becomes a character.
if / elseThe condition inside if () must evaluate to TRUE or FALSE. Only the matching block runs.
forFor each value in conditions, the code inside { } runs with cond set to that value.
Note
In practice, tidyverse functions like group_by() + summarise() often replace explicit loops — but loops are essential when you need more custom control.
Functions take inputs (arguments) and return an output. They’re how you avoid copy-pasting the same code repeatedly.
Try it!
zscore that standardizes a vector: (x - mean(x)) / sd(x)c(12, 5, 22, 8, 17, 3, 30)na.rm argument that passes through to mean() and sd()Always check what you loaded:
This dataset has one row per participant per emotion condition — Subject_ID, Emotion, meanVivid, meanAccuracy, Age, Age_Group, Gender, BDI_Sum.
|>The pipe sends the output of one function as the input to the next. Read it as “and then.”
Warning
Always ungroup() when you’re done — forgetting causes mysterious bugs later.
Try it!
Age_Group? Use count()meanVivid) for each Emotion condition?meanAccuracy) for each combination of Emotion and Age_Group?Subject_ID) had the highest mean vividness across all conditions?{ggplot2} builds plots in layers:
Every plot needs: data, aesthetic mapping (aes()), and at least one geom.
| Data | Good geoms |
|---|---|
| One continuous variable | geom_histogram(), geom_density() |
| One categorical variable | geom_bar() |
| Continuous × categorical | geom_boxplot(), geom_violin() |
| Continuous × continuous | geom_point(), geom_smooth() |
Try it!
meanVivid as a histogram, colored by Emotionfacet_wrap(~ Emotion) — does the shape differ across conditions?Age (x) vs. meanVivid (y)geom_smooth(method = "lm") — is there a relationship with age?| Verb | What it does |
|---|---|
select() |
Keep or drop columns |
filter() |
Keep rows matching a condition |
arrange() |
Sort rows |
mutate() |
Create or transform columns |
summarise() |
Collapse to summary statistics |
group_by() |
Apply operations within groups |
select() and filter()mutate() and arrange()case_when() — Multiple Conditionscase_when() is if/else for whole columns — much cleaner than nesting ifelse() calls.
Try it!
Age_Group == "Younger Age" and meanVivid is not missingvivid_z that z-scores meanVividEmotionarrange(desc(...)) — which emotion condition has the highest vividness?Data is tidy when:
vivid_df_avg.csv is already tidy — one row per participant per emotion condition.
Wide (one row per participant)
| Subject_ID | Negative | Neutral | Positive |
|---|---|---|---|
| P001 | 5.2 | 4.1 | 3.8 |
| P002 | 6.1 | 3.9 | 5.0 |
Long (one row per observation)
| Subject_ID | Emotion | meanVivid |
|---|---|---|
| P001 | Negative | 5.2 |
| P001 | Neutral | 4.1 |
| P001 | Positive | 3.8 |
| P002 | Negative | 6.1 |
ggplot2 and dplyr generally want long format.
pivot_longer() and pivot_wider()# Long → Wide (e.g., one column per emotion condition)
df_wide <- df |>
select(Subject_ID, Emotion, meanVivid) |>
pivot_wider(
names_from = Emotion,
values_from = meanVivid
)
# Wide → Long (back to original shape)
df_long <- df_wide |>
pivot_longer(
cols = c(Negative, Neutral, Positive),
names_to = "Emotion",
values_to = "meanVivid"
)separate()Useful when one column contains two pieces of information mashed together.
When data about the same participants lives in separate files, we join them.
# All four joins — the key is which rows to keep
inner_join(df_trials, df_demo, by = "Subject_ID") # only matched rows
left_join(df_trials, df_demo, by = "Subject_ID") # all trials, matched demographics
right_join(df_trials, df_demo, by = "Subject_ID") # all demographics, matched trials
full_join(df_trials, df_demo, by = "Subject_ID") # everything from bothNote
Use by = c("subj" = "id") when the key column has different names in each table.
| Situation | Join |
|---|---|
| Only participants with complete data from both files | inner_join |
| All trials; add demographics where available | left_join |
| Combine all records, fill gaps with NA | full_join |
left_join is the most common in practice.
Descriptive statistics summarize what’s in your sample: means, SDs, counts.
Inferential statistics ask: could this pattern have occurred by chance?
Report: t(df), p-value, and Cohen’s d as the effect size.
afexaov_ez automatically applies Greenhouse-Geisser correction and reports generalized eta-squared (ges) as the effect size.
Look at: coefficient estimates (direction and size), p-values (significance), R² (variance explained).
Try it!
meanAccuracy between Negative and Neutral conditionsgeom_boxplot() + geom_jitter()aov_ez()Statistics asks: is there a relationship?
Machine learning asks: can I predict new outcomes?
Both use regression — but ML focuses on generalizability: does the model work on data it has never seen?
Warning
Evaluating a model on the same data it was trained on is overly optimistic. We need held-out data for a fair test.
caret| Metric | Meaning |
|---|---|
| R² | Proportion of variance explained (0–1, higher = better) |
| RMSE | Average prediction error in original units (lower = better) |
| MAE | Mean absolute error (lower = better) |
Cross-validation gives a more honest estimate than fitting and testing on the same data.
Every project this quarter followed the same arc:
Import → Inspect → Tidy → Wrangle → Visualize → Analyze → Communicate
These aren’t separate skills — they’re one continuous workflow.
| Package | What you used it for |
|---|---|
readr |
read_csv() — import data |
dplyr |
filter, mutate, summarise, joins |
tidyr |
pivot_longer, pivot_wider, separate |
ggplot2 |
All visualization |
afex / emmeans |
ANOVA and follow-up tests |
caret |
Cross-validated model fitting |
# Wrangle
df_neg <- df |>
filter(Emotion == "Negative") |>
mutate(bdi_category = case_when(
BDI_Sum < 14 ~ "Minimal",
BDI_Sum < 20 ~ "Mild",
BDI_Sum >= 20 ~ "Moderate/Severe"
))
# Visualize
ggplot(df_neg, aes(x = bdi_category, y = meanVivid)) +
geom_boxplot() +
geom_jitter(width = 0.1, alpha = 0.4) +
labs(title = "Negative Memory Vividness by Depression Level",
x = "BDI Category", y = "Mean Vividness") +
theme_bw()
# Analyze
model <- lm(meanVivid ~ BDI_Sum + Age, data = df_neg)
summary(model)That’s a full research workflow. All in one quarter!
|> makes complex operations readable — read it as “and then”You now have the foundations to: