Getting started with R
Foundation – for arriving with no R at all
This is the first module of the companion curriculum, and it is written for the person who arrives with no R at all: a student, a professional from another field, or anyone curious about how the course analysis is built. It does not assume you have programmed before. By the end you will know what R is, what the editor around it does, how to store and reshape data, how to draw figures from it and read the ones that the course shows you, how the package system of R and its main dialects fit together, and where the course materials pick up from here.
As in the Quarto module, every command below is shown on the page and left for you to run. R is a language you learn by typing into it and watching what comes back, so the way to use this page is to try each snippet yourself, either in the zero-install browser lab or in your own copy of R. Nothing on this page executes when it renders.
1. R and the IDE
R is a programming language built for working with data: reading it in, reshaping it, fitting statistical models, and drawing figures. It is free, open source, and the common language of quantitative social science, which is why the course analysis is written in it. R is the thing that does the calculation, and on its own it is just a program that reads instructions and returns answers.
The IDE is the program you drive R from. An IDE (an integrated development environment) is the working environment around R, a window with your files on one side, a place to write and run code, and panels that show results, figures and the objects you have created. Three are in common use for R, and the course supports all of them: Positron, the newer environment from the company behind the most widely used R tools, which is the default for the course and what the facilitator drives on the shared screen; RStudio, the one most R users learned on; and VS Code, the general-purpose editor that Positron is built from. Newcomers often conflate the language and the environment, although each is installed separately. You install R once, so the language is present on your machine. You install the IDE once, as the place you work, and when you first open it the IDE finds the R you installed and connects to it.
You do not strictly need an IDE to use R – R has its own bare console – but a modern IDE makes everything easier to see and to keep, so the course assumes one, and this module reads the same in any of the three. The setup page walks through installing R and each IDE, and the IDE module sets the three side by side, pane by pane.
You do not have to install anything to begin. Open the browser lab: a full copy of R starts up inside the tab after a few seconds, with no editor and no install to manage. Most of the sections below can be typed straight into it, which makes it the fastest way to follow along on a first read. The lab holds the panel data/EUframes_cy.csv but not data/analysts5.csv or data/spec_grid.csv, so the snippets that read those two files, in sections 5 and 9, will not run there.
2. The console and the script
There are three surfaces you type R into, and knowing which is for what saves a great deal of confusion later.
The console is the interactive prompt. You type one line, press Enter, and R answers immediately. It is where you try things out, check a value, or run a quick calculation. Its weakness is that the moment you close the session, everything you typed there is gone, and there is no record of how you reached a result.
A script is a plain text file, with the extension .R, that holds a sequence of commands you want to keep. Instead of typing into the console and losing it, you write the commands in the script and run them from there – all at once, or line by line. A script is a record: it runs the same way tomorrow and on someone else’s machine, and it shows step by step how a result was produced. The course follows a simple rule here. Try things in the console, but the moment something matters, move it into a script.
A Quarto document, with the extension .qmd, is a script and a written report in one file. It holds ordinary prose (the sentences you are reading now are written this way) interleaved with ‘chunks’ of R code. Render the document and R runs every chunk, dropping its results – tables, figures, numbers – into the finished report at the point where they belong. This is the unit of work that the course is built around, because it keeps the analysis and the writing that explains it in a single file, so that the two cannot drift apart. Your exercise report is a Quarto document, and rendering it is how your preregistration block gets a timestamp.
3. Objects and data frames
R works by putting values into named objects and then referring to them by name. You assign a value to a name with the arrow <- (type it as a less-than sign followed by a hyphen). After that, the name stands in for the value everywhere you use it.
unemployment_rate <- 7.5 # a single number
country <- "Germany" # text, called a 'character string', is quoted
in_crisis <- TRUE # a logical value: TRUE or FALSE
unemployment_rate # typing the name prints the value backMost data is not a single value but a column of them. A vector is an ordered run of values of one type, built with c() (for ‘combine’). Arithmetic and summary functions work on the whole vector at once, which is why R rarely needs the loops that other languages reach for.
unemp <- c(5.8, 5.6, 4.9, 4.1, 5.8) # unemployment across five years
length(unemp) # how many values
mean(unemp) # their average
unemp * 100 # every element multiplied, in one stepYou reach into a vector with square brackets, either by position or – far more useful in analysis – by a condition, which keeps only the elements where the condition is TRUE.
unemp[1] # the first value
unemp[unemp > 5] # only the years above five per centA data frame is how R stores a table: a set of equal-length vectors sitting side by side as columns, one row per observation. Almost everything in the course is a data frame. You pull a single column out by name with the $ operator, and a rectangular block with [rows, columns].
# A tiny data frame built by hand, three country-years:
d <- data.frame(
country = c("Austria", "Austria", "Greece"),
year = c(2004, 2005, 2010),
unemp = c(5.8, 5.6, 12.7)
)
d # the whole table
d$unemp # one column, as a vector
d[1:2, ] # the first two rows, all columns4. Packages
Everything so far – c(), mean(), data.frame() – ships with R itself, a core known as base R. Everything else arrives as packages: bundles of functions, their documentation, and sometimes data, written by researchers and developers around the world and published on CRAN, the Comprehensive R Archive Network. CRAN holds over twenty thousand of them. When the next section reads a file with read_csv(), that function comes from a package called readr, not from R itself, and the sections after that lean on packages for nearly everything.
Using a package takes two steps, and the two of them trip nearly every newcomer at least once:
install.packages("readr") # step 1: download it to your machine – once, ever
library(readr) # step 2: load it into the session – once, each sessioninstall.packages() fetches the package from CRAN and stores it in the package library on your machine. You run it once and never again, until you want an update. library() takes an installed package and attaches it to the running session so its functions are available by name, and it belongs at the top of every script that needs it. Installing is buying the book, and library() is taking it off the shelf. Confuse the two and you meet the most common error messages in R. there is no package called 'readr' means step 1 never happened on this machine, while could not find function "read_csv" usually means step 2 is missing from the current session.
Two habits around packages are best adopted from the start. First, put every library() call at the top of the script, together. The block then doubles as a plain statement of what the script depends on, and anyone who opens the file to reproduce the work sees at a glance what they need to install. Second, know the :: form: readr::read_csv() calls a function through its package name without attaching anything, and you will meet it in code written by people who want the package behind every function to be visible on the line that uses it.
In the course workspace, the script R/check_setup.R checks every package that the exercise needs and, if any are missing, prints the ready-made line that installs them.
Run library(fakepackagename) and read the error carefully, then run library(ggplot2) (installed by the setup check in the workspace, and preinstalled in the browser lab) and notice that success is silent. Learning to read error messages calmly is part of learning R. This one says nothing more alarming than ‘not on the shelf’, and it is the message you will see most often.
5. Reading data
You will rarely type data by hand. Most data arrives as a CSV – a ‘comma-separated values’ file, a plain text table where each line is a row and commas divide the columns. The course ships its data this way because a CSV is plain text, which any program can open and which version control can compare line by line. The file that matters most here is EUframes_cy.csv, the country-year panel that every specification in the course is built on.
R reads a CSV into a data frame in one call. Base R has read.csv(); read_csv() from the readr package is a little faster and reports the column types it inferred, and it is the one that the course uses.
library(readr)
euframes <- read_csv("data/EUframes_cy.csv") # a project-relative path
euframes # print the table
nrow(euframes) # 270 country-years
names(euframes) # the column namesNote the path: "data/EUframes_cy.csv" is written relative to the project folder, not as C:/Users/yourname/Desktop/.... A relative path works on any machine that has the project, whereas an absolute path breaks the moment that the work moves. Opening your workspace as a project and writing every path relative to it costs nothing, and it keeps the work runnable on someone else’s machine.
Two functions let you check what you have just read without printing the whole thing:
head(euframes) # the first few rows
summary(euframes$unemp) # min, median, mean and max of one columnRead data/analysts5.csv into an object called analysts with read_csv(), then run nrow(analysts) and names(analysts). It holds the five Multi100 analysts’ recorded results, one row each, and as the smallest file in the project it is a good one to print whole and look at properly. Compare its column list against euframes. One file describes country-years and the other describes analyses, and what a row is in each is the first question to ask of any dataset.
Other formats
The Eurobarometer surveys behind the panel, which are the raw material of the course, are distributed by the GESIS data archive as Stata (.dta) and SPSS (.sav) files, because those are the packages that much of survey research historically ran on. R reads both through the haven package, one function per format (the file names below are illustrative – none of these files ship with the course):
library(haven)
eb <- read_dta("eurobarometer_wave.dta") # Stata; read_sav() reads SPSSThese formats differ from a CSV in storing labelled data. A Stata or SPSS survey file records that a column called eu_meaning contains the answers to the question “What does the EU mean to you?”, and that its stored values 1, 2, 3 stand for named answer options. haven keeps all of that: labelled columns print with their question text attached, and as_factor() turns the numeric codes into their labels when you want to work with the words rather than the numbers.
eb$eu_meaning # prints with its question and value labels attached
as_factor(eb$eu_meaning) # codes become their labels: a factorMany organisations keep their data in Excel files, and the readxl package reads them one sheet at a time, since a workbook is really a stack of tables:
library(readxl)
rates <- read_excel("unemployment_rates.xlsx", sheet = "annual")Treat what arrives from a spreadsheet with mild suspicion. Excel displays one thing and stores another often enough – dates especially – that column types should be checked with summary() straight after every import.
Finally, Parquet and Feather are the newer columnar formats, which store a table column by column, and you will increasingly meet them where datasets are large or move between programming languages. They are compressed and fast, and unlike a CSV they store the type of each column inside the file, so nothing has to be re-inferred on reading. The arrow package reads both, and the small nanoparquet package reads and writes Parquet without needing any other package:
library(arrow)
panel <- read_parquet("EUframes_cy.parquet") # read_feather() for FeatherWhatever the source format, what you end up with in R is the same data frame, and everything downstream – the verbs of the next sections, the models fitted in later modules – proceeds identically. Every read_ function here also has a write_ counterpart (write_csv(), write_dta(), write_parquet(), and write_xlsx() from the writexl package), so R also serves as the converter between all of these formats. The course ships CSV all the same, because at 270 rows the efficiency of the binary formats buys nothing, while plain text can be opened in anything, compared line by line or read in an email.
Fetching data from a repository or an API
Most data come from an archive, a statistical office or a repository, and for many of those sources an R package fetches the data straight into a data frame. The query is then written down in your script instead of being clicked through on a website, and that is what makes the download repeatable. The data sources module maps the sources themselves, while this subsection covers the packages that query them, which fall into three groups.
Indicators from the international organisations and statistical offices, which are open and need no account. The WDI package (and wbstats, which covers more of the World Bank databases) queries the World Bank indicators by their codes, which is how the macro variables of the course panel were obtained:
library(WDI)
macro <- WDI(country = c("DE", "FR", "GB"),
indicator = c(unemp = "SL.UEM.TOTL.NE.ZS", growth = "NY.GDP.MKTP.KD.ZG"),
start = 2004, end = 2013)The eurostat package does the same for the Eurostat tables (get_eurostat("une_rt_a") fetches the annual unemployment table, with restatapi as an alternative that uses the newer API, the interface through which programs request data from Eurostat); OECD and the general-purpose rsdmx query the OECD and any other service that publishes in SDMX, the standard format in which many statistical agencies exchange data; Rilostat reaches ILOSTAT; onsr the Office for National Statistics; wid the World Inequality Database. Each returns a long, tidy table, one row per country and period, that pivots into whatever shape a model needs.
Survey and census microdata behind a registration. These packages log in for you, so the credentials belong in an environment variable or a keyring, never in the script. ipumsr builds and downloads IPUMS extracts through the IPUMS API and reads them with their labels; gesisdata downloads studies from the GESIS archive by study number with your GESIS login, which is the reproducible way of fetching the Eurobarometer waves behind this course; rdhs does the same for the Demographic and Health Surveys, pewdata for the files of the Pew Research Center, and manifestoR for the coded programmes of the Manifesto Project, with an API key. The essurvey package was written for the previous download site of the European Social Survey, and the survey has since moved to a new portal, so check that it still works before building on it. Whatever the package, the file that arrives is usually Stata or SPSS, and haven reads it with its labels, as the previous subsection showed.
Repositories of deposited work. osfr reads and writes the Open Science Framework through its API and needs no login for a public node, which is how the script R/get_data.R in the workspace fetches the course panel; dataverse downloads a dataset from any Dataverse installation by its DOI; zen4R does the same for Zenodo, and deposits there as well. For a source with no package, httr2 sends the request and jsonlite parses the answer, and rvest reads a table out of a web page, with the caution that scraping a site is subject to its terms and to the courtesy of not hammering it.
Two habits keep a fetched dataset reproducible. Write the fetch as a function or a script of its own, save what it returns as a dated file, and read that file in the analysis, so that the analysis does not depend on the network being up or the source being unchanged. And record the version or the download date in the file name or the README, because the World Bank and Eurostat revise past years without renaming the series.
6. The pipe
Once the data are read in, most of the work is reshaping them, and reshaping is almost always a sequence of steps. The pipe, written |>, exists to make such sequences readable. It takes whatever is on its left and passes it as the first argument to the function on its right, so instead of wrapping functions inside one another and reading them inside-out, you write the steps top to bottom in the order they happen:
# without the pipe: innermost first, hard to follow
round(mean(euframes$unemp), 1)
# with the pipe: left to right, in the order it happens
euframes$unemp |> mean() |> round(1)On one short line the gain is small. Over the multi-step chains of the next section the gain grows with every step, which is why nearly all modern R is written this way. You will also meet an older pipe in other people’s code. |> has been part of R itself only since 2021, and older code uses %>%, a pipe from a package called magrittr that does nearly the same job. Read the two identically, and write the native |>, as all the course materials do.
7. Three dialects
R is old enough to have grown dialects, families of packages with their own vocabulary and style, all doing the same underlying work. You will meet three – base R, the tidyverse and easystats – and this section sets the same small task in each, so that none of them ever looks foreign. The task is to keep the crisis-period rows of the panel, with a handful of columns.
Base R does it with the bracket-and-$ machinery of section 3, no packages required:
crisis <- euframes[euframes$year >= 2009, c("country", "year", "unemp", "mcosmo")]It works, and code written this way twenty years ago still runs. Its weakness is that the brackets spell out the mechanics of the selection, so the reader must decode positions and conditions to reconstruct what the line is for.
The tidyverse is the dominant modern dialect, a family of packages that share one design philosophy and can be installed and attached under the single name tidyverse (Wickham et al. 2019). You have already met one of its members, readr, and the second is dplyr, which gives each common data operation a named verb:
filter()keeps rows that meet a condition;select()chooses columns;mutate()adds or changes a column;summarise()collapses many rows into a summary.
Chained with the pipe, the same task reads as a list of named steps:
library(dplyr)
euframes |>
filter(year >= 2009) |> # crisis-period rows only
select(country, year, unemp, mcosmo) # the columns of interestmutate() derives a new column from existing ones, and summarise() reduces the table to a single row (or, with group_by(), one row per group). The pipeline below adds a plain-language crisis flag, groups the country-years by whether a bailout programme was active, and reports the average cosmopolitan-framing score in each group:
euframes |>
mutate(crisis = year >= 2009) |> # a new TRUE/FALSE column
group_by(bailout) |> # split by bailout status
summarise(
n_cells = n(), # how many country-years
mean_cosmo = mean(mcosmo), # their average outcome
mean_unemp = mean(unemp)
)Read the chain aloud and it is almost English: take the panel, add a crisis flag, group by bailout status, and for each group report the count and two averages. Readability of that kind serves reproducibility, because a reviewer who can read the code can check it, and that is why the course scripts are built from these verbs.
easystats is the third dialect, which the course facilitator uses and recommends. It is likewise a family of packages under one installable name, easystats (Lüdecke et al. 2022), but it is built mainly for what happens after a model is fitted, whereas the tidyverse is at its best preparing data before one. It has a data-preparation member too – datawizard, whose verbs are modelled on those of dplyr –
library(datawizard)
euframes |>
data_filter(year >= 2009) |>
data_select(c("country", "year", "unemp", "mcosmo"))– but its main contribution begins once a model exists, and that is the subject of section 10. The three dialects are used together, in layers. Base R is always underneath, the tidyverse gives data work a grammar of named verbs, and easystats gives the modelling stage one of its own.
Write a chain on euframes that keeps only Greece (filter(country == "Greece")), selects year, unemp and mcosmo, and prints the result. Then extend it with summarise() to report Greece’s mean unemployment across the whole period. If you are curious, redo the first step in base R and in datawizard, and notice that you can already read all three.
8. The grammar of graphics
Section 7 introduced the tidyverse as a family and named two of its members. ggplot2 is the third, and it is the one that draws (Wickham et al. 2019). Almost every chart that the course draws from its own data is built with it – the charts on the slides, the live class chart on the results page, the small country panels in the browser lab – so reading ggplot2 code is how you satisfy yourself that a figure shows what its caption says it shows. The exceptions are the interactive charts on the estimand choices page, which Observable Plot draws in the browser, and two figures in the statistics module: the rotating plane of section 2, drawn with rgl, and the forest plot of section 10, drawn by metafor.
The name abbreviates ‘the grammar of graphics’, the idea that any chart can be described as a combination of a few standard parts, much as a grammar describes how words combine into sentences. ggplot2 therefore has no separate function for each chart type. It splits a figure into parts that recombine freely: the data, the mapping from columns to visual properties, the geometric shapes drawn, the scales that decide what those shapes look like, the panels that the plot is divided into, and last the labels and the styling. A scatterplot and a bar chart are two fillings of the same slots, and once you know the slots you can read the figures on this site, including kinds you have not met yet.
Data, mapping and geometry
A plot needs data, a mapping, and at least one geom. Everything else has a default good enough to start with.
library(ggplot2)
ggplot(euframes, aes(x = unemp, y = mcosmo)) +
geom_point()Three things happen there. ggplot() receives the data frame and declares which columns drive the picture. aes() is the aesthetic mapping, and it states that the unemployment rate governs horizontal position while the cosmopolitan-framing score governs vertical position. geom_point() adds a geom, a layer of geometric shapes, drawing one point per row – 270 points for 270 country-years. Axis ranges, tick marks, gridlines and the panel background are all inferred from the data, which is why this bare version is already a readable figure.
Layers are joined with +, never with the pipe |> of section 6. The reason is historical, since ggplot2 settled on + years before R had a native pipe. The practical consequence is that a long chain changes operator exactly once, at the point where it stops reshaping data and starts drawing, and typing |> where + belongs is among the commonest first-week errors in R.
Mapping and setting
Every visual property of a layer can be given two ways, and confusing them is the next commonest error. Put a property inside aes() and it is mapped: its value is read from a column, it varies row by row, and ggplot2 adds a legend explaining the correspondence. Put the same property outside aes(), as an ordinary argument to the geom, and it is set, taking one fixed value for the whole layer, with no legend because there is nothing to explain.
# MAPPED: colour comes from a column, so the points differ and a legend appears
ggplot(euframes, aes(x = unemp, y = mcosmo, colour = factor(bailout))) +
geom_point()
# SET: colour is a constant, identical for every point, and no legend appears
ggplot(euframes, aes(x = unemp, y = mcosmo)) +
geom_point(colour = "#0e3b6a", alpha = 0.5)alpha controls opacity, running from 0 for invisible to 1 for solid, and setting it below 1 is the standard remedy for a scatterplot where points pile up on one another. The constant "#0e3b6a" is a hex colour code, the dark blue that the course figures use.
Leaving out the factor() wrapped around bailout produces the wrong figure without any warning. bailout is stored as 0 and 1 in numeric form, so colour = bailout gives ggplot2 a number, which it treats as continuous, drawing a colour gradient and a legend running smoothly from 0.00 to 1.00. What you wanted was two categories in two colours, and factor(bailout) is how you say so. The same applies to year, which is also numeric: map it raw for a gradient across the decade, or wrap it in factor() for ten discrete groups.
Layers
A figure can have as many layers as you need, each with its own geom, and they are drawn in the order written, so later layers cover earlier ones.
ggplot(euframes, aes(x = unemp, y = mcosmo)) +
geom_point(alpha = 0.4, colour = "#848c94") +
geom_smooth(method = "lm", colour = "#0e3b6a", fill = "#0e3b6a", alpha = 0.15)The grey points are drawn first and the trend line over them. geom_smooth() fits a line through the cloud and draws it with a band showing the uncertainty around it, and method = "lm" asks for a straight line from the same least-squares method that the statistical methods module uses to fit models. R then prints the message `geom_smooth()` using formula = 'y ~ x', which only reports the relationship that was fitted and needs no action. Reference layers behave the same way: geom_hline(yintercept = 0) rules a horizontal line at zero, and section 9 uses one to split a figure into results that support a claim and results that do not.
Line thickness is set with linewidth, whereas point size and type size are set with size, and mixing the two up is behind a great many warnings.
geom_line(linewidth = 0.4) # the thickness of a drawn line
geom_point(size = 0.7) # the area of a plotted point
geom_text(size = 3.5) # the height of a piece of typePassing size to a line geom still works, but R warns “Using size aesthetic for lines was deprecated in ggplot2 3.4.0. Please use linewidth instead.” The reason that the two names survive side by side is that size on a point means an area and on text a height, neither of which is a line thickness.
Scales
The mapping says which column drives colour; the scale decides which colours it drives, and the same holds for positions, sizes and shapes. Every mapping gets a default scale, and you override it whenever the default is not what a reader needs.
ggplot(euframes, aes(x = unemp, y = mcosmo, colour = factor(bailout))) +
geom_point(alpha = 0.8) +
scale_colour_manual(
name = "Assistance programme",
values = c("0" = "#848c94", "1" = "#a10000"),
labels = c("0" = "none", "1" = "bailout")
) +
scale_x_continuous(breaks = seq(0, 30, by = 5))scale_colour_manual() names a colour for each level of the variable. Giving values and labels as named vectors, with "0" = … rather than a bare pair, means the assignment cannot silently reverse itself if the data ever arrives in a different order. The colours that the course uses throughout are #0e3b6a (dark blue), #E69F00 (orange), #009E73 (green), #a10000 (dark red) and #848c94 (mid grey), with #c7ccd2 reserved as the muted grey for results that are not statistically significant. The orange and green belong to a palette built to stay distinguishable under the commonest forms of colour vision deficiency, which is why the deck never rests an argument on a red-green contrast alone.
Position scales follow the same override logic, and one of their arguments catches people out. scale_x_continuous(breaks = …) decides where the tick marks fall, but the sibling argument limits removes every row outside the range before any statistic is computed, so a smoother is refitted on whatever rows remain.
p_trend <- ggplot(euframes, aes(x = unemp, y = mcosmo)) +
geom_point(alpha = 0.4, colour = "#848c94") +
geom_smooth(method = "lm", colour = "#0e3b6a")
p_trend + scale_y_continuous(limits = c(0.30, 0.50)) # drops rows, then refits
p_trend + coord_cartesian(ylim = c(0.30, 0.50)) # keeps every row, only zoomsRun the first of those and R warns “Removed 163 rows containing non-finite outside the scale range (stat_smooth())”, and the trend line you are looking at now runs through 107 country-years rather than 270. coord_cartesian() zooms the finished figure instead and leaves the computation untouched, so it is the one to use when you only want a closer look.
Facets
Faceting repeats the same plot once per group, on shared axes, so the groups can be compared by eye instead of by disentangling overlapping series. A country-year table suits it well, since each country can have a panel of its own.
ggplot(euframes, aes(x = year, y = mcosmo)) +
geom_line(colour = "grey55", linewidth = 0.4) +
geom_point(size = 0.7) +
facet_wrap(~ cntry, ncol = 6) +
scale_x_continuous(breaks = c(2004, 2008, 2013))Twenty-seven small panels appear, one per country code, each showing that country’s cosmopolitan-framing series across the decade. The browser lab draws this figure early in the exercise, so that the crisis years are in view before any model is fitted and it is plain how differently the countries move. The ~ cntry is a one-sided formula, with nothing to the left of the ~. The lm() call in section 10 uses the same syntax with an outcome written on the left. ncol fixes the shape of the grid. Where two grouping variables matter, facet_grid(rows = vars(a), cols = vars(b)) crosses them into a matrix of panels.
Labels and themes
Two layers finish a figure. labs() supplies the words – axis titles, plot title, subtitle, caption, legend name – and it deserves more care than beginners give it, because a column name such as mcosmo means nothing to a reader who has not opened the codebook. The theme then governs everything with no data behind it: type size, gridlines, background, the position of the legend.
ggplot(euframes, aes(x = unemp, y = mcosmo)) +
geom_point(alpha = 0.4, colour = "#848c94") +
geom_smooth(method = "lm", colour = "#0e3b6a", fill = "#0e3b6a", alpha = 0.15) +
labs(
x = "Unemployment rate (%)",
y = "Cosmopolitan framing (share of respondents)",
title = "Where unemployment is higher, cosmopolitan framing is lower",
caption = "270 country-years, EU-frames dataset"
) +
theme_minimal(base_size = 12) +
theme(
panel.grid.minor = element_blank(),
plot.title = element_text(face = "bold"),
legend.position = "top"
)theme_minimal() is the default look across the course, and base_size is its most useful argument, scaling every piece of type at once: 9 for a dense panel of small multiples, 12 for a page figure, 15 and upwards for a slide. A bare theme() afterwards overrides individual pieces, and the order matters, because a complete theme applied after your adjustments would wipe them out. Inside it, element_blank() deletes a piece outright, element_text() restyles type, and element_line() restyles rules and gridlines.
A plot is an ordinary R object, so you can store it under a name, extend it later, and write it to a file:
p <- ggplot(euframes, aes(x = unemp, y = mcosmo)) +
geom_point(alpha = 0.4, colour = "#848c94") +
labs(x = "Unemployment rate (%)", y = "Cosmopolitan framing")
p + geom_smooth(method = "lm") + theme_minimal(base_size = 12)
ggsave("figures/framing_unemployment.png", p, width = 7, height = 5, dpi = 300)ggsave() takes its dimensions in inches and writes whichever format the file extension names. When you do not name a plot, it saves the last one drawn, which is convenient but can save the wrong figure, so name the object.
Draw the scatterplot of unemp against mcosmo, then change one thing at a time and watch what moves. Swap geom_point() for geom_text(aes(label = cntry)) and each point becomes its country code; move colour = factor(bailout) from inside aes() to outside it and read the error it raises; drop the factor() and compare the two legends. Then add facet_wrap(~ year) and decide for yourself whether ten panels tell you more than one crowded cloud does.
From the pipe to the plot
Reshaping and drawing usually happen in one chain, with dplyr passing its result straight to ggplot2. The chain uses |> while it is still working on data and switches to + once ggplot() appears.
library(dplyr)
euframes |>
filter(year >= 2009) |>
group_by(cntry) |>
summarise(mean_unemp = mean(unemp)) |>
ggplot(aes(x = reorder(cntry, mean_unemp), y = mean_unemp)) +
geom_col(fill = "#0e3b6a") +
labs(x = NULL, y = "Mean unemployment, 2009–2013 (%)") +
theme_minimal(base_size = 11)reorder() sorts the country codes by the value being plotted rather than alphabetically, which turns a jumble of bars into a ranking you can read at a glance. geom_col() draws a bar whose height is the number stored in the data, which is what you want once summarise() has already done the counting; its cousin geom_bar() counts the rows for you instead, and reaching for the wrong one of the two is a frequent small frustration.
Where two figures belong side by side, the patchwork package composes whole plots with the same + that adds layers. Attach it with library(patchwork), then p1 + p2 sets two plot objects in a row and patchwork::wrap_plots(plots, nrow = 1) does the same for a list of them.
Build a chain that keeps the crisis years, groups by year, and reports the mean of mcosmo and the mean of mneg per year, then pipe it into a plot with two geom_line() layers in different colours. You will find yourself wanting a legend and not getting one, because both colours were set rather than mapped. The remedy is to reshape the data into long form, with one row per year and outcome, using pivot_longer() from the tidyr package, and then to map colour to the column that names the outcome.
9. Reading the class chart
The central figure of the course is a specification curve, and you now have every part needed to build one yourself. A hypothesis about a dataset admits many defensible analyses, and if you fit all of them and line the results up in order, you can see the range of answers that the data supports instead of the single answer that one analyst happened to report.
data/spec_grid.csv holds that universe for the EU-frames case: 840 rows, one per defensible analysis, each recording the partial correlation it produced, whether that result reached statistical significance, and the choices that distinguish it from its neighbours. Reading it needs one preparatory step, which the paragraph after the code explains.
library(readr)
library(dplyr)
library(ggplot2)
grid <- read_csv("data/spec_grid.csv")
curve <- grid |>
mutate(r_ca = ifelse(positive_outcome, r, -r)) |> # claim alignment
arrange(r_ca) |>
mutate(rank = row_number())
baseline_row <- filter(curve, baseline)The claim under test is that economic hardship depresses cosmopolitan framing of the EU, so on a positively worded outcome such as mcosmo a negative correlation supports it. Three of the six outcomes in the grid are negative framings, where support means a positive correlation instead, and plotting the raw numbers together would scatter agreeing results on opposite sides of zero. Claim alignment repairs that. Flip the sign wherever the outcome is a negative framing, and afterwards a negative value means support for every row alike. positive_outcome is the flag recording which is which, ifelse() performs the flip, and the _ca suffix is the naming habit of the course for any claim-aligned quantity. Sorting by the aligned value and numbering the rows then gives each specification a rank, and that rank becomes the horizontal axis.
ggplot(curve, aes(x = rank, y = r_ca)) +
geom_hline(yintercept = 0, linewidth = 0.4, colour = "#848c94") +
geom_point(aes(colour = sig), size = 0.9) +
geom_point(data = baseline_row, colour = "#a10000", size = 3) +
annotate(
"text",
x = baseline_row$rank, y = baseline_row$r_ca - 0.09,
label = "the specification actually submitted\nt = -3.804",
colour = "#a10000", fontface = "bold", size = 3.2, hjust = 0.1
) +
scale_colour_manual(
name = NULL,
values = c("TRUE" = "#0e3b6a", "FALSE" = "#c7ccd2"),
labels = c("TRUE" = "p < .05", "FALSE" = "not significant")
) +
coord_cartesian(ylim = c(-0.75, 0.32)) +
labs(
x = "specification, ranked by claim-aligned partial correlation",
y = "claim-aligned partial correlation",
title = "840 defensible analyses of one hypothesis"
) +
theme_minimal(base_size = 12) +
theme(legend.position = "top", panel.grid.minor = element_blank())Almost everything in that block has come up already: a reference line, a layer of small points coloured by a mapped logical column, a second point layer drawn from a different data frame, a manual colour scale, a zoom, labels and a theme. data = baseline_row inside the geom overrides the frame given to ggplot(), and that is how a single row gets picked out and emphasised. annotate() is the one function you have not met. It adds a mark that comes from no column at all, since you supply the coordinates and the text yourself, and that is why its position is read off baseline_row rather than mapped.
Every point below the grey line is an analysis that supports the claim. Of the 840 analyses, 68.7% lie below it and 63.9% reach conventional significance. The aligned correlations run from −0.70 to +0.24, so the universe contains both firm support and results pointing the other way. The red point is the specification actually submitted to the Multi100 project, the one with the t-statistic of −3.804 that the course reproduces in Part 3, and its rank is 418, with 49.6% of the universe more negative than it, almost exactly the middle. The submitted analysis was a typical one, neither an outlier nor a cherry-pick. Yet the same defensible menu, worked through in good faith by any competent analyst, would have supported a great many other numbers.
Rebuild the curve, then colour the points by outcome instead of by sig, dropping the scale_colour_manual() layer as you go, since its two named colours cannot serve six outcomes. The clumping you see is why the choice of outcome gets a fork of its own on the course menu. Then try facet_wrap(~ estimator) on the same figure and ask yourself which of the two pictures you would rather a reviewer saw.
10. Naming the estimand
Fitting models is the subject of the statistical methods module, where every model is read through the summary() printout of base R. Nothing that follows is needed for the course tasks. It is here because it completes the map of the dialects, and because it ties the tools of R to the habit that the course asks of you throughout, which is to name the quantity you want before you estimate it.
Suppose the model exists, a plain linear model of cosmopolitan framing on unemployment and growth. The statistical methods module goes through this kind of output line by line.
m <- lm(mcosmo ~ unemp + growth, data = euframes)In base R you call summary(m) and read the printout. The easystats packages instead give you one function for each question you might ask of the fitted model:
library(easystats)
model_parameters(m) # the coefficient table as a tidy data frame, with confidence intervals
model_performance(m) # fit indices – R2, AIC and friends – in one row
check_model(m) # the assumption dashboard: residual plots, normality, influential rows
report(m) # the model written out in publishable sentencesmodel_parameters() returns the coefficients as a data frame that you can filter, join or plot (Lüdecke et al. 2020); check_model() draws the standard diagnostic checks in one figure (Lüdecke et al. 2021); and report() writes the results paragraph itself, with the numbers drawn live from the object. In a rendered Quarto document the prose then cannot fall out of step with the model, as a number pasted in by hand can.
One member of the family, modelbased, builds that habit into its functions (Makowski et al. 2025). The coefficients in a table are whatever quantities the parameterisation of the model happens to produce, whereas these functions make you name the quantity you want – the estimand – and then compute that quantity from the model:
euframes <- euframes |> mutate(bailout = factor(bailout))
m2 <- lm(mcosmo ~ unemp + bailout, data = euframes)
estimate_slopes(m, trend = "unemp") # the unemployment slope, with its uncertainty
estimate_means(m2, by = "bailout") # expected framing with and without a bailout programme
estimate_contrasts(m2, contrast = "bailout") # the difference between those two expectationsAn expected outcome under each condition, a slope, a contrast between scenarios: each is a quantity you can state in words before touching the software. (Under the bonnet these functions call the marginaleffects package, which does the job that Stata users will know from margins.) Saying what you want to estimate first, and only then deriving the analysis that estimates it, is what the course practises in Part 2, with the estimand and the causal graph, and again in Task 5, where you state your specification and the quantity that it estimates in the preregistration block of your report before running it.
11. The workspace and the browser lab
Everything above is preparation for the workspace in which the day’s work is done, and for the browser lab page on which it can be tried first.
The workspace. You install R and an IDE and download a single zip file, a ready-made project folder holding the data, a set of short R/ scripts, and a report skeleton called index.qmd. That report skeleton is your first real script. It is a Quarto document you open, read, and fill in as the day goes, running its chunks and rendering it to a finished report. The smaller scripts around it do one job each – R/check_setup.R confirms your installation works, R/get_data.R fetches the data, R/spec_tools.R holds the helpers that fit your specifications and submit your results – and reading them is a gentle way to see the vocabulary of this module used in earnest. The setup page has the download and the step-by-step.
The browser lab. The browser lab runs the same exercise inside a web tab, with a full copy of R that starts up on its own and the data already in place. It is the place to try most of this page before you have installed anything, the page that the facilitator drives on the shared screen, and the fallback if your installation fails on the day. It gives the same numbers as the workspace.
In either place, the sequence is the one you have just practised on a small scale: read the data, reshape it with a few verbs, draw it to see what you are dealing with, then fit a model and read what it says. The rest of the course repeats that sequence, with more care taken over each step.
Sources and further study
This module was written for the course and adapts no existing lesson, so there is no borrowed text to attribute. It teaches only the slice of R that the course materials use, in the order that the day needs it, plus the map of dialects that puts those materials in context.
If you want a fuller, self-paced grounding in R after this, these open resources are recommended as further reading:
- The Carpentries, “R for Reproducible Scientific Analysis” and “Programming with R”. https://swcarpentry.github.io/r-novice-gapminder/ and https://swcarpentry.github.io/r-novice-inflammation/. Both are licensed CC BY 4.0 and are the standard hands-on introductions to R for researchers, covering objects, vectors and data frames in far more depth than this page attempts.
- The Turing Way, “Guide for Reproducible Research”. https://book.the-turing-way.org/. Licensed CC BY 4.0. Not an R tutorial as such, but the best short statement of why the habits that this module gestures at – scripts over console history, relative paths, one file for analysis and report – are worth building from the first day.
- The package documentation sites, which for the packages of sections 5 to 10 are readable introductions as well as references: tidyverse.org (with the free book R for Data Science at r4ds.hadley.nz), ggplot2.tidyverse.org, easystats.github.io/easystats, haven.tidyverse.org, readxl.tidyverse.org, and the arrow R package site at arrow.apache.org/docs/r.
When you are ready for your first statistical model, the module on the statistical methods behind the course starts with a single lm() on the panel that you have just learned to read and reshape, and works up from there to the models that the case requires.