Chris Collins

Lab · Playground

R playground

Real R running in your browser through webR — vectors, data frames, a linear model with its summary, simulation, and base-graphics plots.

Code blocks on this page are editable — change them and press Run.

R is the statisticians' language. It grew out of S, written at Bell Labs in the 1970s so that analysts could explore data interactively instead of writing Fortran, and Ross Ihaka and Robert Gentleman started R in 1993 as a free reimplementation. Its habits come from that history: everything is a vector, missing values are part of the data, and fitting a model and reading its summary takes two lines.

These blocks run R itself, compiled to WebAssembly by the webR project, in a Web Worker. The first Run on the page downloads about 20 MB, so it takes a few seconds; after that runs are quick. As at the R console, each top-level expression that isn’t assigned prints its value, and plots appear as images below the output. Each block starts afresh.

Vectors: arithmetic happens on whole vectors at once
rain_mm <- c(Jan = 78, Feb = 61, Mar = 55, Apr = 41, May = 30, Jun = 12,
             Jul = 4, Aug = 6, Sep = 18, Oct = 47, Nov = 83, Dec = 92)

rain_in <- round(rain_mm / 25.4, 2)     # every element, no loop
rain_in

rain_mm[rain_mm > 50]                    # index with a logical vector
names(which.max(rain_mm))                # the wettest month
cumsum(rain_mm)[c("Jun", "Dec")]         # running total at mid-year and year end
rain_mm - mean(rain_mm)                  # the mean is "recycled" across all 12
Missing values are part of the language
readings <- c(21.4, 22.1, NA, 23.0, NA, 22.6)

mean(readings)                 # NA: one unknown makes the answer unknown
mean(readings, na.rm = TRUE)   # unless you say to drop them
is.na(readings)
sum(is.na(readings))           # TRUE counts as 1

# Fill gaps with the value before them
filled <- readings
for (i in which(is.na(filled))) filled[i] <- filled[i - 1]
filled
Data frames: a table whose columns are vectors
rides <- data.frame(
  day      = c("Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun", "Mon", "Wed", "Sat"),
  km       = c(12.5, 8.0, 15.2, 0, 9.8, 42.0, 25.5, 11.0, 14.1, 38.4),
  minutes  = c(38, 25, 47, 0, 31, 128, 80, 35, 44, 119),
  commute  = c(TRUE, TRUE, TRUE, FALSE, TRUE, FALSE, FALSE, TRUE, TRUE, FALSE)
)
rides <- subset(rides, km > 0)               # drop the day off
rides$kmh <- round(rides$km / (rides$minutes / 60), 1)

str(rides)
rides[order(-rides$km), ][1:3, ]             # the three longest rides
aggregate(cbind(km, kmh) ~ commute, data = rides, FUN = mean)
Fitting a model: lm() and its summary
# mtcars ships with R: 32 cars from a 1974 magazine road test
fit <- lm(mpg ~ wt + hp, data = mtcars)
summary(fit)

# Predict fuel economy for a 3,000 lb car with 150 hp, with a 95% interval
predict(fit, newdata = data.frame(wt = 3.0, hp = 150), interval = "prediction")

The summary is R at its most characteristic: coefficients with standard errors, t-values and stars for significance, the residual spread, and R². Each extra 1,000 lb costs about 3.9 mpg with horsepower held fixed.

A plot: base graphics show up as an image
# Old Faithful: eruption length against the wait before it (another built-in dataset)
plot(faithful$waiting, faithful$eruptions,
     pch = 19, col = "#3b6ea5",
     xlab = "Minutes since the last eruption", ylab = "Eruption length (minutes)",
     main = "Old Faithful")
abline(lm(eruptions ~ waiting, data = faithful), col = "#c0392b", lwd = 2)
cor(faithful$waiting, faithful$eruptions)
Simulation: is this die loaded?
set.seed(2026)
loaded <- c(1, 1, 1, 1, 1, 1.6)                 # six comes up 60% more often
rolls  <- sample(1:6, 600, replace = TRUE, prob = loaded)

table(rolls)
chisq.test(table(rolls))                        # a small p-value says "not fair"

# How often would 600 fair rolls give a count of sixes at least this high?
sixes <- replicate(5000, sum(sample(1:6, 600, replace = TRUE) == 6))
mean(sixes >= sum(rolls == 6))                  # 0: not once in 5,000 tries
hist(sixes, breaks = 30, col = "grey85", border = "white",
     main = "Sixes in 600 fair rolls", xlab = "count")
abline(v = sum(rolls == 6), col = "#c0392b", lwd = 3)
Functions, closures, and the apply family
# A function that returns a function, remembering its arguments
make_scaler <- function(lo, hi) function(x) (x - lo) / (hi - lo)
to_unit <- make_scaler(0, 40)
to_unit(c(0, 10, 25, 40))

# sapply runs a function over each element and simplifies the result
stats <- function(x) c(n = length(x), mean = mean(x), sd = sd(x))
sapply(split(mtcars$mpg, mtcars$cyl), stats)    # mpg by number of cylinders

# Vectorised conditions instead of if/else in a loop
ifelse(mtcars$mpg[1:8] > 20, "thrifty", "thirsty")