Skip to main content

R for Journalists: A UK Practical Guide

Clean, analyse and chart UK public data with R and the tidyverse — and keep every step reproducible so an editor can check your working.

Last reviewed: Next review due:

1. What is R and why journalists use it

R is a free, open-source language built for working with data. Where a spreadsheet records the result of a calculation, R records the instructions— so your analysis is a script you can re-run, review and correct. That reproducibility is the single biggest reason journalists learn it: when a figure is challenged, you can point to the exact line that produced it.

Most reporters never touch base R directly. They use the tidyverse, a family of packages with a consistent, readable grammar for reading, cleaning, joining, summarising and charting data. Download R from r-project.org and the RStudio editor from posit.co.

2. Installing R, RStudio and the tidyverse

Install R itself first (from CRAN via r-project.org), then RStudio Desktop from Posit — RStudio is the workbench, R is the engine underneath. Open RStudio and install the tidyverse once from the console:

# run once to install, then load in every session
install.packages("tidyverse")
library(tidyverse)

The tidyverse bundles readr (reading files), dplyr (wrangling), tidyr (reshaping) and ggplot2 (charts), among others. You only install.packages() once per machine, but you library() at the top of every script.

3. Loading data with readr

Read a CSV into a data frame with read_csv(), then use glimpse() to see the columns and their types before you do anything else.

library(readr)

# a council spending CSV downloaded to your project folder
spending <- read_csv("council-spending.csv")

glimpse(spending)   # columns, types and a preview
nrow(spending)      # confirm the row count matches the source

read_csv() guesses column types and warns you when a column is messy — a useful early signal that a “number” column contains stray text like N/A or a pound sign.

4. The core dplyr verbs

Almost all journalistic analysis is five verbs: filter rows, select columns, mutate to add or change a column, group_by to split into groups, and summarise to collapse each group to a single number.

library(dplyr)

spending |>
  filter(amount > 25000) |>                 # payments above the disclosure threshold
  select(supplier, service_area, amount) |> # keep only the columns you need
  mutate(amount_k = amount / 1000) |>        # a derived column, in thousands
  group_by(service_area) |>
  summarise(
    total    = sum(amount),
    payments = n()
  ) |>
  arrange(desc(total))

n() counts the rows in each group, and arrange(desc()) sorts largest first. Read the chain top to bottom: filter, then trim columns, then derive, then group, then total.

5. The native pipe |>

The pipe |> takes whatever is on its left and feeds it in as the first argument on its right, so you never nest functions inside out. These two lines do exactly the same thing:

# nested, read inside-out
summarise(group_by(spending, service_area), total = sum(amount))

# piped, read top-to-bottom
spending |>
  group_by(service_area) |>
  summarise(total = sum(amount))

The native |> ships with R 4.1 and later and needs no packages. You will also see the older tidyverse pipe %>% from magrittr in tutorials; for new code, prefer the native pipe.

6. Joining two datasets

Joins link tables on a shared key — for example, attaching population figures to crime counts so you can compute a fair rate. A left_join() keeps every row on the left and pulls in matches from the right, leaving NA where nothing matched.

# crimes has a force_code column; population has one too
rates <- crimes |>
  left_join(population, by = "force_code") |>
  mutate(rate_per_1000 = offences / population * 1000)

# rows with no population match surface as NA -- worth checking
rates |> filter(is.na(population))

Always inspect the unmatched rows after a join. Missing matches usually mean inconsistent codes or names — and that gap can be the story, not just a nuisance.

7. Charts with ggplot2

ggplot2 builds a chart in layers: the data, an aes() mapping of columns to axes, and one or more geom_ layers. This draws a horizontal bar chart of total spend by service area.

library(ggplot2)

by_area <- spending |>
  group_by(service_area) |>
  summarise(total = sum(amount))

ggplot(by_area, aes(x = reorder(service_area, total), y = total)) +
  geom_col() +
  coord_flip() +
  labs(title = "Council spend by service area",
       x = NULL, y = "Total (GBP)")

reorder() sorts the bars, coord_flip() turns them horizontal for readable labels, and labs() sets the titles. Export with ggsave("chart.png").

8. Reproducible reports with R Markdown and Quarto

Rather than typing commands into the console, put them in a document that mixes narrative and code. Rendering it re-runs every step and regenerates the tables and charts from the raw data. R Markdown files use the .Rmd extension; Quarto (.qmd) is the newer, cross-language successor from Posit.

# render a Quarto document to HTML or PDF
quarto::quarto_render("analysis.qmd")

# the R Markdown equivalent
rmarkdown::render("analysis.Rmd")

Keep the source data untouched, do all cleaning in code, and anyone with the file and the data can reproduce your numbers exactly — the standard an editor or a corrections process needs.

9. Where UK journalists use R

  • 1Parsing council transparency returns and spend-over-threshold CSVs, then updating monthly by re-running one script.
  • 2Reshaping NHS waiting-time, A&E and workforce data from wide, awkward spreadsheets into tidy tables.
  • 3Summarising ONS releases such as population, earnings and inflation, and recomputing rates per head.
  • 4Joining Home Office police-recorded crime to population figures for like-for-like comparisons between forces.
  • 5Producing consistent, house-styled charts with ggplot2 for the final published graphic.

10. Red flags

  • A numeric column read as text because it contains commas, pound signs or 'N/A' -- clean it before you sum it.
  • NA values silently dropped by sum() unless you write sum(x, na.rm = TRUE); decide what missing means first.
  • Editing the raw CSV by hand -- always clean in code so the change is recorded and repeatable.
  • Rows lost in a join because keys differ in case or spacing; check the unmatched rows every time.
  • Copying results out of the console instead of rendering a document, so nobody can reproduce the figure.

11. Getting-started checklist

  • I have installed R from CRAN and RStudio Desktop from Posit.
  • I have run install.packages('tidyverse') once and library(tidyverse) at the top of my script.
  • I have checked column types with glimpse() and confirmed the row count against the source file.
  • I have done all cleaning in code and left the raw download untouched.
  • I have inspected the unmatched rows after every join.
  • I have written the analysis in a Quarto or R Markdown document so it renders end to end.

Find the data

R is only as good as the data you feed it. Browse UK public datasets, or use our FOI Request Builder when the numbers you need have not been published.

Common mistakes

  • Forgetting to library() a package at the top of a fresh session, then hitting 'could not find function'.
  • Using = where == is meant inside filter(); a single equals is assignment, not comparison.
  • Assuming summarise() keeps grouping -- it drops one level, so re-group if you need to.
  • Reading numbers as factors or text and getting nonsense when you try to average them.
  • Hard-coding a file path that only exists on your laptop instead of a project-relative path.

Related guides

Frequently asked questions

Do I need to be a programmer to use R?
No. R is a statistical language, not a general software-engineering tool, and the tidyverse packages were designed to read almost like plain English. If you can write a spreadsheet formula, you can learn the handful of dplyr verbs that cover most journalism: filter, select, mutate, group_by and summarise. Start by reproducing an analysis you have already done in Excel, then extend it. RStudio gives you autocomplete, a data viewer and a plotting pane, so you are never editing code blind. Most reporters are productive within a week of daily practice.
What is the difference between R and Python for journalism?
Both are excellent and the choice is largely cultural. R was built by statisticians, so its charting (ggplot2) and statistical functions are first-class, and the tidyverse offers a very consistent grammar for wrangling data. Python is a general-purpose language that also does data work well through pandas, and it wins if you also need scraping, automation or web apps. Many newsrooms use both. If your work is mostly cleaning, analysing and charting public datasets, R and the tidyverse are hard to beat. Pick one, learn it properly, and you can always add the other later.
What is the pipe operator and why does everyone use it?
The pipe passes the result of one step straight into the next, so you can read an analysis top to bottom instead of nesting functions inside out. R has a native pipe written as |>, available since R 4.1; the tidyverse also popularised the magrittr pipe %>%. Both let you write a chain such as filter, then group, then summarise as separate lines. This makes analysis far easier to read, review and correct, which matters when an editor or a fact-checker needs to follow exactly how you reached a number. For new code the native |> is the sensible default.
How do I make my R analysis reproducible for an editor?
Put your code in a Quarto (.qmd) or R Markdown (.Rmd) document rather than typing commands into the console. These weave narrative text, code and output into one file, so rendering it re-runs every step and regenerates the charts and tables from the source data. If a figure is challenged, you can point to the exact line that produced it. Keep the raw data unedited, do all cleaning in code, and commit the document to version control. Anyone with the data and the file can reproduce your result byte for byte, which is the gold standard for defensible data journalism.
Where do UK journalists actually use R?
Anywhere the dataset is too big or the analysis too repetitive for a spreadsheet. Reporters use R to parse council spending returns, to reshape NHS waiting-time and workforce data, to summarise ONS releases such as population or earnings, and to join police-recorded crime to population figures for like-for-like rates. Because the analysis is scripted, a monthly or quarterly update means re-running the same file against the new download. R also shines for the final chart: ggplot2 produces clean, consistent graphics you can restyle to a house look and export straight into a story.