Learning R · Data Analysis

Learning R for Data Analysis in Journalism

I officially started to learn R in 2026 Spring (self taught a little bit before). This is the notes from course J298 Reporting Inequality with Data at UC Berkeley's Graduate School of Journalism.

Rggplot2dplyrtidyversetidycensus
// Outline
Chapter 1
Setup & Packages

R uses packages to extend its capabilities — think of them like apps you install once and open whenever you need them. The install step only happens once; the library step happens every session.

R · install once
install.packages("tidyverse")  # data wrangling
install.packages("dplyr")      # R's version of SQL
install.packages("ggplot2")    # graphics
install.packages("psych")      # descriptive stats
install.packages("tidycensus") # census data API
install.packages("janitor")    # data cleaning
install.packages("modelr")     # regression helpers
R · load every session
library(tidyverse)
library(dplyr)
library(ggplot2)
library(psych)
library(tidycensus)
library(janitor)
library(modelr)

Installing a package is like going to the hardware store to buy a tool. Using library() is getting it out of your toolbox. You don't go back to the store every time you need it.

Loading Data

Everything in R lives in a data frame — a table with rows and columns. Use read_csv() to load CSV files. The <- arrow assigns the result to a name you choose.

R
ind <- read_csv('globalind.csv', col_names = TRUE)
view(ind)   # opens a spreadsheet-like viewer
str(ind)    # shows structure: column types, row count

The global indicator dataset has 222 rows (countries) and 10 columns including GNI, health expenditure, education spending, unemployment rate, and maternal mortality. The first two columns are character strings; the rest are numeric.

Select & Filter

The pipe operator %>% chains commands together — read it as "and then." select() picks columns; filter() picks rows; arrange() sorts.

R · select & sort
ind %>%
  select(country, code, gni) %>%
  arrange(desc(gni))          # sort by GNI, highest first
R · filter rows
filter(ind, country == "United States")   # exact match
filter(ind, gni > 60000)                  # numeric condition
R · chain it all together
ind %>%
  select(country, code, gni, skulspend) %>%
  filter(gni >= 50000) %>%
  arrange(desc(skulspend))

Shortcut for %>%: Shift + Cmd + M

Descriptives

Before running any statistics, understand your data's shape. Are the mean and median close? Is there missing data? The psych package's describe() gives you everything at once.

R
mean(ind$gni, na.rm = TRUE)    # na.rm ignores missing values
median(ind$gni, na.rm = TRUE)

describe(ind)                  # mean, sd, min, max, skew...

If you forget na.rm = TRUE and your data has missing values, R will return NA instead of the actual result. Always add it when working with real-world datasets.

Correlation

A correlation tells you how strongly two variables move together — from -1 (perfect inverse) to +1 (perfect positive). Use pairwise.complete.obs to handle missing data gracefully.

R · single correlation
cor(ind$skul, ind$skulspend, use = "pairwise.complete.obs")
R · correlation matrix (all variables)
round(cor(ind[3:10], use = "pairwise.complete.obs"), 2)
Histograms & Scatterplots

Visualizing before analyzing is essential — it shows you whether data is skewed, whether outliers exist, and whether a relationship actually looks linear.

R · histogram
hist(ind$gni)
hist(ind$healthexp)

# Positively skewed? Try log transformation to normalize:
hist(log10(ind$gni))
R · scatterplot
plot(ind$gni, ind$healthexp)   # X = independent, Y = dependent

# Multiple variables at once:
pairs(~ gni + healthexp + skul, data = ind)

On log transformation: Taking log10() of a right-skewed variable can normalize it and improve your model — but it makes results harder to explain to readers. Use it when the math requires it, not by default.

Chapter 2
Regression Basics

Once you've identified a strong correlation, linear regression quantifies the relationship — how much does Y change for every one-unit increase in X? In R, use lm() (linear model). The formula reads: dependent ~ independent.

R · run regression
options(scipen = 999)   # turns off scientific notation

ind_mod1 <- lm(formula = healthexp ~ gni, data = ind)
summary(ind_mod1)
R · multiple regression
ind_mod2 <- lm(formula = healthexp ~ gni + unemp, data = ind)
summary(ind_mod2)
Reading the Output

The summary() output has a lot going on. Here's what to focus on:

R-squared: Tells you how much of the variation in Y is explained by X. An R² of 0.87 means 87% of the change in health expenditure is explained by GNI. Check the p-value to confirm it's statistically significant (below 0.05).

Coefficients: The coefficient for GNI was 0.107 — for every $1 increase in GNI, health expenditure is expected to rise by about 11 cents.

The coefficient always describes a one-unit change in the independent variable. Multiply by a round number to make the interpretation more intuitive for your audience.

Residuals & Outliers

A residual is the difference between the model's predicted value and the actual value. Large residuals are cases that behave unexpectedly — which is often where the most interesting stories are.

R · add predictions and residuals
schools_pr <- schools %>%
  add_predictions(school_mod1) %>%
  add_residuals(school_mod1)

# Standardize to find true outliers (z-score > 2 or < -2)
schools_pr$z_score <- as.numeric(scale(schools_pr$resid))
R · scatterplot with regression line
plot(schools$poorpct, schools$p_passed,
     main = "Poverty vs. Test Pass Rate",
     xlab = "% in poverty", ylab = "% passed",
     pch = 19)
abline(school_mod1, col = "red")