R → Julia
Translating the book’s walk-throughs
Use this page for the Julia equivalents of the book’s R idioms. Project-specific exceptions are linked from the relevant walkthroughs.
Reading data
| R | Julia |
|---|---|
read.csv("f.csv") |
CSV.read("f.csv", DataFrame) |
read.csv("f.csv", skip = 1) |
CSV.read("f.csv", DataFrame; header = 2) |
read.csv("f.csv", na.strings = "***") |
CSV.read("f.csv", DataFrame; missingstring = "***") |
read_excel("f.xlsx") |
XLSX.readtable("f.xlsx", "Sheet1") \|> DataFrame |
setwd("...") |
not needed — rawpath() resolves from the repo root |
head(df) |
first(df, 6) |
str(df) |
describe(df) |
nrow(df) / ncol(df) |
nrow(df) / ncol(df) |
names(df)[1] <- "Year" |
rename!(df, 1 => :Year) |
skip = 1 becomes header = 2, not skipto. R counts lines to throw away; CSV.jl names the line the header is on. Off-by-one here silently turns your header row into data.
Subsetting and reshaping
| R | Julia |
|---|---|
df[df$Month == 6, ] |
subset(df, :Month => ByRow(==(6))) |
subset(df, Year >= 1951 & Year <= 1980) |
subset(df, :Year => ByRow(y -> 1951 <= y <= 1980)) |
df[, c("Jun", "Jul", "Aug")] |
df[:, [:Jun, :Jul, :Aug]] |
unlist(df[, c("Jun", "Jul")]) |
vec(Matrix(df[:, [:Jun, :Jul]])) |
merge(a, b) |
innerjoin(a, b; on = :Year) |
spread(key = Sex, value = Value) |
unstack(df, :Sex, :Value) |
reduce(full_join, frames) |
reduce((a, b) -> outerjoin(a, b; on = [:Country, :Year]), frames) |
lag(x) / lead(x) |
vcat(missing, x[1:end-1]) / vcat(x[2:end], missing) |
factor(x) |
categorical(x) |
cut(x, breaks) |
cut(x, breaks) (CategoricalArrays.jl) |
na.rm = TRUE |
skipmissing(x) |
merge(a, b) with no by guesses the join key from shared column names. innerjoin makes you name it. The explicit version is worth the extra characters — a silent join on the wrong shared column is a hard bug to see.
Summary statistics
| R | Julia |
|---|---|
mean(x) |
mean(x) |
var(x) |
var(x) |
sd(x) |
std(x) |
quantile(x, c(0.3, 0.7)) |
quantile(x, [0.3, 0.7]) |
mean(x > 0.5) |
mean(x .> 0.5) |
cor(x, y) |
cor(x, y) |
mosaic::mean(y ~ g) |
combine(groupby(df, :g), :y => mean) |
mosaic::var(y ~ g) |
combine(groupby(df, :g), :y => var) |
table(x) |
freqtable(x) or countmap(x) |
Two that agree where you might expect them not to:
varis the sample variance in both, dividing byn − 1. R has no option; Julia’svar(x; corrected = false)gives the population form. The book means the sample variance, so the default is right.quantileuses the same default. R’stype = 7and Julia’s default are the same linear-interpolation rule, so deciles match exactly. R offers nine types and Julia takesalpha/betaparameters; neither default needs touching here.
The mosaic package’s formula interface (mean(anomaly ~ period)) is the one idiom with no direct Julia counterpart. groupby + combine does the same job more verbosely and more explicitly — you name the output column, so a table of grouped statistics arrives already labelled.
Time-series filters
| R | Julia |
|---|---|
mFilter::hpfilter(x, freq = 6.25) |
solve (I + λD′D) * trend = x, then cycle = x - trend |
Project 13 implements the standard penalized least-squares definition directly: D is the second-difference matrix and λ is 6.25 for annual data. The helper checks that years are contiguous before filtering.
Charts
| R | Julia (Makie / AlgebraOfGraphics) |
|---|---|
plot(x, y, type = "l") |
lines(x, y) |
plot(x, y) |
scatter(x, y) |
lines(x, y2) |
lines!(ax, x, y2) |
abline(h = 0) |
hlines!(ax, 0) |
abline(v = 1980) |
vlines!(ax, 1980) |
text(x, y, "label") |
text!(ax, x, y; text = "label") |
legend("topleft", ...) |
Legend(fig[1, 2], ax) or axislegend(ax) |
hist(x, breaks = seq(...)) |
hist(x; bins = ...), or freqtable_binned + barplot |
col =, lwd =, xlab = |
color =, linewidth =, xlabel = |
mfrow = c(2, 2) |
fig[i, j] grid positions |
ts(x, start = c(1880, 1), frequency = 12) |
keep Year/Month columns; build a Date |
The ! suffix is the whole mental model: lines creates a new axis, lines! adds to an existing one. R’s plot / lines split does the same thing without saying so.
In Julia, keep Year and Month columns, construct a real date when needed, and let the axis follow from the data.
The dual-axis chart
Walk-through 1.8 plots temperature and CO₂ on one set of axes with two different y-scales, using par(new = TRUE) to draw the second series over the first and axis(side = 4) to give it its own scale on the right.
Project 1 replaces it with two panels: aligning two scales is arbitrary and can change the visual impression without changing any number.
Three alternatives:
- Two stacked panels sharing an x-axis; each series keeps its own units.
- Index both to a common base —
index_to(x, 1)puts both at 100 in the first period — and plot on one axis. Requires both series to have a meaningful zero, which rules it out for anomaly data (see Project 1 Q3.4). - State the correlation coefficient, which is what the question asks for.
Project 1 uses the two-panel form and reports the correlation.
Soft scope: the one that actually bites
R’s for loop body shares the global environment, so this accumulates:
total <- 0
for (i in 1:3) total <- total + i # total is 6At top level, the direct Julia translation creates a new local instead:
total = 0
for i in 1:3
total += i # warns, and creates a NEW local each iteration
end
total # still 0Julia’s soft scope rule: a for, while or comprehension body at global scope treats assignment to an existing global as creating a local instead. It emits a warning, but a warning is easy to miss and the value is silently wrong — or the name is undefined when you read it back.
Three fixes, in order of preference:
# 1. Wrap in `let` - the counter becomes a real local, no warning
result = let
total = 0
for i in 1:3
total += i
end
total
end
# 2. Build it functionally and skip the accumulator
total = sum(1:3)
# 3. Declare `global` - works, but a sign the code wants restructuring
total = 0
for i in 1:3
global total += i
endInside a function this problem does not exist, because function bodies are hard scope. It only appears at top level — which is exactly where notebook and script code lives.
A trap specific to this repository: Quarto’s Julia engine wraps each cell in a way that makes the naive version work, so code that renders correctly on these pages can still fail when copied into a plain .jl script. Both Project 3 and Project 4 hit this. The pages use let blocks and up-front construction so that the code works either way.
let is fix #1 only when you initialise inside it
Fix #1 above works because total = 0 sits inside the let. Reach for let to rebind an outer name and it fails, which is the opposite trap:
df = load()
let keep = df.x .> 0
df = df[keep, :] # UndefVarError: `df` not defined in local scope
endlet is a hard scope: df = ... creates a local, so the right-hand side reads that local before it has a value. Julia raises an error, which is safer, but the message can make a plainly defined outer variable look missing.
Project 8 hit this while filtering four survey waves in sequence. The fix is a function that takes a frame and returns a new one:
function require_complete(df, cols; asked_in)
keep = .!in.(df.wave, Ref(asked_in)) .| complete_in(df, cols)
return df[keep, :]
end
clean = require_complete(clean, ["income"]; asked_in = ["2008-2010"])The rule of thumb: let for a value you build and hand back, a function for a transformation you apply repeatedly. Neither needs global.
Long tables lose their middle
Quarto’s Julia engine displays each cell’s result through an IOContext carrying :limit => true. DataFrames honours that the way it would for a terminal: past about 26 rows it prints the head and tail and replaces everything between with a ⋮ row.
On a web page there is no reason to elide anything, and the failure is quiet — the table looks complete unless you count the rows. Project 8 shipped a 46-country table with 20 countries hidden, and the prose discussed three of the hidden ones.
show_all from DoingEconomics forwards the display with the limit off:
show_all(country_table) # every row, instead of head + ⋮ + tailTwo details matter in its implementation. It defines show for exactly the MIME types DataFrames implements, not a generic show(::IO, ::MIME, ::AllRows) — Quarto picks an output format by attempting show rather than by consulting showable, so a catch-all method advertises text/markdown, gets chosen, and dies inside the forwarded call. And showable delegates to the wrapped table so the choice stays honest. test/runtests.jl pins both.
Missing values
| R | Julia |
|---|---|
NA |
missing |
is.na(x) |
ismissing(x) |
mean(x, na.rm = TRUE) |
mean(skipmissing(x)) |
na.strings = "***" |
missingstring = "***" |
complete.cases(df) |
dropmissing(df) |
The difference that matters: R’s aggregate functions mostly default to propagating NA and offer na.rm to opt out; Julia has no such flag, and you wrap the input in skipmissing instead. Julia’s version is noisier to write and harder to get wrong by accident — you cannot drop missing data without a visible skipmissing at the call site.
Numbers in the data
The GISS temperature file writes values as -.37 rather than -0.37, and codes missing as ***. R’s read.csv handles the leading-dot form because as.numeric does; CSV.jl parses it too. Neither needs special handling, but it is worth knowing the file looks like that before concluding a parse has gone wrong.