Week 13: Multi-lingual Analysis (Python/R)
2026-07-09
group-reflection.qmdPart 1: Statistical Programming Foundations
Part 2: Working with Real World Data
Part 3: Advanced Topics & Summary
plotly, leaflet, DT) add interactivity to any Quarto HTML output — no server requiredggplotly() is the quickest path from a ggplot2 plot to an interactive one{sf} + geom_sf() for choropleth and boundary maps with proper projection handling{geofacet} for small-multiple comparisons arranged in a geographic layoutAny ggplot2 figure becomes explorable with ggplotly() — hover, zoom, toggle groups:
{reticulate}
Expectations for this session
You are not expected to become proficient in Python today. You should leave being able to
reticulate does and how it works,NA as a first-class citizen, formulas (y ~ x), data frames built inSumming a million numbers — a for loop versus the vectorised built-in:
elapsed
0.522
Tip
Vectorised R functions call fast compiled C code. The R interpreter is slow; the libraries are not. Python has exactly the same split: plain loops are slow, numpy is fast.
When you change part of an object, R makes a fresh copy rather than editing the original in place:
[1] 1 2 3
Tip
This makes R safe and predictable: nothing changes unless you assign it. The cost: for a very large dataset, every small edit can duplicate it — slow and memory-hungry. pandas/numpy edit data in place instead, trading a little safety for speed.
So far we have written functions: data goes in, a result comes out. As programs grow, two wishes appear:
That is object-oriented programming (OOP):
Tip
You have used this all semester without naming it: summary() prints group means for a data frame but coefficients and p-values for a model. One command, many behaviours — the object decides.
Most languages give you one way to build classes and methods. R has several — and the oldest (S3) trusts you so completely you can lie about a class:
Error in z$rank: $ operator is invalid for atomic vectors
| System | Style | Used by |
|---|---|---|
| S3 | informal, convention-based | most of base R, dplyr, ggplot2 |
| S4 | formal classes + validation | Bioconductor, Matrix |
| R6 | encapsulated, mutable | shiny, plumber |
| S7 | successor unifying S3/S4 | new packages (emerging) |
Some fields are simply Python-first — the reference implementations live there:
scikit-learn’s uniform API, Hugging Face transformersNote
The reverse holds too: mixed models, survey statistics, econometrics, and publication-grade graphics are R-first. That asymmetry is exactly why multi-lingual workflows exist.
numpy, pandas, matplotlib/seaborn, scikit-learn — not in the core language| R | Python | |
|---|---|---|
| Designed for | statistics & data analysis | general-purpose programming |
| Data frame | built in | pandas library |
| Missing values | NA in the core language |
NaN/None, library-dependent |
| Indexing | starts at 1 | starts at 0 |
| Assignment | x <- 1 |
x = 1 |
| OOP | S3/S4/R6/S7 | one class system |
| Typical style | functions + pipes: df |> f() |
methods on objects: df.f() |
| Killer apps | inference, ggplot2, Quarto/Shiny |
ML/DL, glue code, web |
Tip
Both are interpreted, dynamically typed, open source, and slow in raw loops but fast through compiled libraries. The concepts you learned this semester transfer — only the syntax changes.
The same computation, side by side — spot the differences:
How would Python write this? (guess before revealing)
R — the atomic vector
Important
Three classic traps: Python counts from 0, slices exclude the end, and lists are not vectorised — x * 2 repeats the list. Element-wise maths needs numpy: np.array(x) * 2.
Python first this time — what would you use in R for each of these?
R — collections
[1] "31 48.15 adelie, gentoo"
| Python | Closest R equivalent |
|---|---|
dict — {"a": 1} |
named list / named vector: list(a = 1) |
tuple — (1, 2) |
no direct equivalent (immutable) |
set — {1, 2} |
unique(x) + union(), intersect() |
list — [1, 2] |
list(1, 2) (R lists hold anything too) |
R
Important
No braces: the indented block is the function body — wrong indentation is a syntax error. And return is mandatory; Python functions don’t auto-return the last expression.
R
dplyr vs pandas — wranglingThe penguins, filtered and summarised — same pipeline, two grammars:
R — functions piped over data
Python — methods chained on the object
dplyr vs pandas — verb dictionary| dplyr | pandas |
|---|---|
filter(mass > 4000) |
df[df["mass"] > 4000] or df.query("mass > 4000") |
select(species, mass) |
df[["species", "mass"]] |
mutate(kg = mass / 1000) |
df.assign(kg = df["mass"] / 1000) |
arrange(desc(mass)) |
df.sort_values("mass", ascending=False) |
group_by(g) |> summarise(m = mean(x)) |
df.groupby("g")["x"].mean() |
left_join(df2, by = "id") |
df.merge(df2, on="id", how="left") |
rename(new = old) |
df.rename(columns={"old": "new"}) |
Tip
Keep this table (and an LLM) at hand and you can read most pandas code today. The verbs are the same ideas — you learned the concepts in week 7, not just the syntax.
ggplot2 vs seaborn — scatterR — grammar of graphics
ggplot2 vs seaborn — distributionsPython first — what’s the ggplot2 equivalent? (guess, then reveal)
R

scikit-learnEvery model — regression, trees, boosting, SVMs — has the same fit/predict API:
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestClassifier
df = penguins.dropna(subset=["bill_length_mm", "flipper_length_mm", "body_mass_g"])
X, y = df[["bill_length_mm", "flipper_length_mm", "body_mass_g"]], df["species"]
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=13)
model = RandomForestClassifier(n_estimators=100).fit(X_train, y_train)
round(model.score(X_test, y_test), 3) # accuracy on held-out data0.965
Also typically easier in Python: deep learning (PyTorch), NLP/LLMs (Hugging Face transformers), gluing APIs and services together. R’s counterpart {tidymodels} is excellent — but the ML ecosystem’s centre of gravity is Python.
Based on: rstudio.github.io/reticulate
{reticulate}?py_run_string(), source_python())import("numpy"))```{r} and ```{python} chunks in one Quarto document — these slides do exactly thatConversion is automatic and (mostly) lossless:
| R | → | Python |
|---|---|---|
single value (1, "a", TRUE) |
↔︎ | scalar (1.0, "a", True) |
vector c(1, 2, 3) |
↔︎ | list [1.0, 2.0, 3.0] |
| matrix / array | ↔︎ | numpy array |
named list list(a = 1) |
↔︎ | dict {"a": 1.0} |
data.frame |
↔︎ | pandas DataFrame |
NULL |
↔︎ | None |
Note
Two bridges, one workspace: in R, py$x reads Python’s x; in Python, r.x reads R’s x. That’s how r.penguins worked earlier.
Define a function in a Python chunk…
…then call it from R via py$, with plain R vectors as arguments:
[1] 3.333333 5.757576 0.000000 10.000000
Tip
For .py files instead of chunks: source_python("helpers.py") makes every function in the file directly callable from R — exactly what you’ll do in the practical.
The good-practice pattern: R wrangles, Python models, R visualises.

No Python chunks needed — import() gives you the module as an R object:
Important
Note the 5L: many Python functions insist on integers, but R numbers are doubles by default — num = 5 would error. The L suffix is the most common reticulate stumbling block.
Reach for R
dplyr, tidyr)ggplot2) & tables (gt)Reach for Python
scikit-learn)transformers)Tip
Good practice: pick one primary language per project and borrow from the other via reticulate where it clearly wins. Document the Python dependency (e.g. a requirements.txt) just like you document R packages with renv.
Tomorrow’s practical, in one slide — given this Python file stats_helpers.py:
Want Python on your own machine? Ways to get it, in order of recommendation:
uv — fast, modern manager for Python versions, environments, and packages
Anaconda / Miniconda — classic data science distribution; heavier, but everything included
python.org installer — not recommended: no environment management, easy to end up with conflicting global packages
Tip
No install needed to experiment: reticulate::py_require("pandas") can provision a Python environment for you automatically (reticulate ≥ 1.41 downloads its own via uv). Details in the practical.
rpy2 embeds R inside Python — reticulate’s mirror image:
pandas, numpy, matplotlib/seaborn, scikit-learn[ ], dictionaries {k: v}, tuples ( ), sets { } — and indexing starts at 0df.groupby(...).mean()) instead of functions on data (aggregate(...))dplyr verbs and ggplot2 layers have direct pandas / seaborn counterparts — the concepts transfer, only the syntax changes{reticulate}py$x, R from Python via r.x; load functions with source_python() or import()reticulate — plus project time for the final submission (22 Jul)Upcoming deadlines
Final submission 22 Jul · Individual contribution statement 23 Jul · Oral exam 29 Jul
source_python() and import()