Skip to content

R: mtcars regression + inline plots

A pure-R notebook: every cell is R. Load the built-in mtcars dataset, summarise it, fit a linear model, and draw two plots that render inline as PNG. Variables flow cell-to-cell through the same content-addressed artifact store the Python cells use; here both ends of every edge happen to be R.

What it shows

  • R cells stand on their own. No Python anywhere. The DAG, the provenance cache, and the cascade all work exactly as they do for Python, language is per-cell, not per-notebook.
  • Inline plots (0.2.0). A ggplot scatter and R's base-graphics 2×2 plot(lm) diagnostic panel both render as PNG in the cell, just like a Python matplotlib figure. A bare trailing ggplot object auto-prints, no explicit print().
  • Two kinds of R→R handoff. data.frames (cars, by_cyl) cross as Arrow IPC. The lm object itself isn't tabular, so it's stored as RDS (r_only) and read straight back by the diagnostics cell with full fidelity, an R-only object flowing between R cells that the Arrow tier couldn't carry. (A Python cell consuming it would get a structured "re-export as a data.frame" error instead of a crash.)
  • renv, one click. The plotting cell needs ggplot2, which isn't in the harness baseline. It's pinned in renv.lock and restored automatically when you open the notebook, or via the Environment panel's Initialize renv. A missing package surfaces a structured install hint, not a stack trace.

Cells

Cell What it does
prep Tidy mtcars into a data.frame (cars), model name column, cyl as a factor.
summarise aggregate() mean mpg / hp / wt by cylinder count → by_cyl.
fit lm(mpg ~ wt + hp + cyl); emit the model (RDS), a tidy coefs table, and one-row model_stats.
plot-mpg ggplot2 scatter of mpg vs weight, coloured by cylinder, with per-group fits → inline PNG.
diagnostics Read model back from RDS; base-graphics 2×2 residual diagnostics → inline PNG.

What you need

  • R on PATH (Rscript). The notebook's arrow, jsonlite, and ggplot2 come from renv.lock: opening the notebook restores them into a project-scoped library automatically; no system installs beyond R itself.
  • The uv-managed Python venv carries only the notebook harness baseline (pyarrow / orjson / cloudpickle); no Python runs any cell here.

Running

From the project root:

uv run strata-notebook --host 127.0.0.1 --port 8765

Open examples/r_mtcars_analysis from the Strata home page and run the cells top to bottom. Or run it headlessly, strata run executes R cells and restores the notebook's renv.lock on the way in:

uv run strata run examples/r_mtcars_analysis

Try this

  1. Swap in another plot. Replace plot-mpg with ggplot(by_cyl, aes(factor(cyl), mean_mpg)) + geom_col(): a bar of mean economy by cylinder. Edit, Shift+Enter, watch it re-render.
  2. Reach a non-tabular object across cells. Add a cell with confint(model): model resolves from the RDS artifact and you get coefficient confidence intervals, no re-fit.
  3. Break, then fix, the environment. Delete the renv/ directory and reopen: the ggplot cell shows the install hint; click Initialize renv (or Install ggplot2) and re-run.

Prepare the mtcars data

kind r

# @name Prepare the mtcars data
#
# A pure-R notebook — every cell is R. Variables flow cell-to-cell
# through the same content-addressed artifact store the Python cells
# use: a data.frame crosses as Arrow IPC, so the next R cell receives
# it as a data.frame with no glue code.
#
# `mtcars` ships with R (1974 Motor Trend, 32 cars). Tidy it into the
# frame the rest of the notebook builds on: keep the model name as a
# real column, make the cylinder count a factor (so the model and the
# plots treat it as categorical), and keep the columns we care about.

cars <- data.frame(
  model = rownames(mtcars),
  mpg = mtcars$mpg,
  wt = mtcars$wt,
  hp = mtcars$hp,
  cyl = factor(mtcars$cyl),
  row.names = NULL
)

cat(sprintf(
  "Prepared %d cars across %d cylinder classes (%s)\n",
  nrow(cars), nlevels(cars$cyl), paste(levels(cars$cyl), collapse = ", ")
))

Summarise by cylinder count

kind r

# @name Summarise by cylinder count
#
# Split-apply-combine in base R: group the cars by cylinder class and
# average economy, power, and weight per group. `aggregate()` is the
# base-R group-by — no dplyr needed. `cars` arrives as a data.frame
# (the Arrow IPC handoff from the prep cell), and `by_cyl` leaves as
# one too, ready for any downstream cell.

by_cyl <- aggregate(
  cbind(mpg, hp, wt) ~ cyl,
  data = cars,
  FUN = mean
)
by_cyl$n <- as.integer(table(cars$cyl))
names(by_cyl) <- c("cyl", "mean_mpg", "mean_hp", "mean_wt", "n")

print(by_cyl)

Fit mpg ~ wt + hp + cyl

kind r

# @name Fit mpg ~ wt + hp + cyl
#
# R's formula syntax in one line: predict mpg from weight, horsepower,
# and the cylinder factor (auto dummy-encoded, 4-cyl as the baseline
# level). Three outputs leave this cell with three different fates in
# the artifact store:
#
#   model        — the lm object itself. Not tabular, so the harness
#                  stores it as RDS and tags it `r_only`. A downstream
#                  *R* cell reads it straight back with readRDS (see the
#                  diagnostics cell); a Python cell consuming it would
#                  instead get a structured "re-export as a data.frame"
#                  error rather than a confusing NameError.
#   coefs        — tidy coefficient table (data.frame -> Arrow IPC).
#   model_stats  — one-row fit summary (data.frame -> Arrow IPC).

model <- lm(mpg ~ wt + hp + cyl, data = cars)
s <- summary(model)

cm <- s$coefficients
coefs <- data.frame(
  term = rownames(cm),
  estimate = cm[, "Estimate"],
  std_error = cm[, "Std. Error"],
  t_stat = cm[, "t value"],
  p_value = cm[, "Pr(>|t|)"],
  row.names = NULL
)

model_stats <- data.frame(
  r_squared = s$r.squared,
  adj_r_squared = s$adj.r.squared,
  sigma = s$sigma,
  df_residual = model$df.residual
)

cat(sprintf(
  "Fitted mpg ~ wt + hp + cyl: R2=%.3f (adj %.3f), residual SE=%.2f mpg\n",
  model_stats$r_squared, model_stats$adj_r_squared, model_stats$sigma
))

Plot mpg vs weight (ggplot2)

kind r

# @name Plot mpg vs weight (ggplot2)
#
# The headline 0.2.0 R feature: plots render inline as PNG, exactly
# like a Python matplotlib figure. A bare trailing ggplot object
# auto-prints (REPL-style), so no explicit `print()` is needed — the
# plot is the cell's last expression and it just shows up.
#
# ggplot2 isn't part of the harness baseline (arrow + jsonlite); it
# comes from this notebook's renv.lock, restored automatically when you
# open the notebook — or one click via the Environment panel's
# "Initialize renv". If it's somehow missing you'll see a structured
# "install ggplot2" hint, not a crash.

library(ggplot2)

ggplot(cars, aes(x = wt, y = mpg, colour = cyl)) +
  geom_point(size = 3, alpha = 0.85) +
  geom_smooth(method = "lm", formula = y ~ x, se = FALSE, linewidth = 0.7) +
  labs(
    title = "Fuel economy falls with weight",
    subtitle = "mtcars, coloured by cylinder count",
    x = "Weight (1000 lbs)",
    y = "Miles per gallon",
    colour = "Cylinders"
  ) +
  theme_minimal(base_size = 13)

Residual diagnostics (base graphics)

kind r

# @name Residual diagnostics (base graphics)
#
# `plot()` on an lm object draws R's canonical 2x2 diagnostic panel
# (residuals vs fitted, Q-Q, scale-location, residuals vs leverage).
# It's a base-graphics plot, and it's captured to PNG the same way the
# ggplot is — no extra code, no device juggling. `par(mfrow)` tiles the
# four panels onto one image.
#
# `model` arrives by reading the RDS artifact the fit cell stored: an
# R-only object (an lm, S3 class and all) flowing from one R cell to
# another with full fidelity — something the tabular Arrow tier can't
# carry. This is the R-to-R counterpart of the cross-language Arrow
# handoff.

op <- par(mfrow = c(2, 2), mar = c(4, 4, 2, 1))
plot(model)
par(op)