prices = [4.99, 12.50, 0.80, 7.25]
total = 0
for p in prices:
if p > 1:
total += p
print(round(total, 2))24.74
Advanced Statistical Programming using R — Multi-lingual Analysis (Python/R)
Leonhard Kestel, Lisa Bondo Andersen, Cynthia Huang
July 10, 2026
Before starting, work through this QUIZ to check your understanding of this week’s lecture on multi-lingual analysis with Python and R.
Yesterday you saw where R reaches its limits, how Python expresses the same ideas with different syntax, and how {reticulate} runs both languages in one workflow. Today you practise exactly the three skills from the lecture: read Python, translate it to R, and call Python from R.
For Exercise 1.1: no — you only read Python code and write R.
For Exercise 1.2 you need a Python that {reticulate} can find. The easiest path: recent versions of reticulate (≥ 1.41) can provision Python for you — just install the R package and it handles the rest on first use:
If you want a proper Python installation of your own (recommended if you plan to use Python beyond today):
uv venv .venv creates a project environment, uv pip install pandas adds packages to itFor each snippet below: first explain in one sentence what it does (read it out loud in your group!), then write R code that reproduces the shown output. You don’t need Python for this — the output is already there to check against.
Snippet A — a loop and a condition:
24.74
It sums all prices above 1 € — += is compound assignment (total <- total + p), and the indented block is the loop body. Idiomatic R skips the loop entirely:
A literal translation with for and if works too, but vectorised subsetting is the R way — the same logic that makes numpy code shorter than plain Python loops.
Snippet B — a list comprehension over a dictionary:
['anna', 'carla']
grades is a dictionary (key → value); the list comprehension collects every key whose value is below 2.0. R’s closest cousin of a dict is a named vector, and the comprehension becomes a filter on names:
Read comprehensions inside-out: for name, g in grades.items() iterates over (key, value) pairs, if g < 2.0 filters, and the leading name says what to keep.
Snippet C — a pandas group-by chain:
mean max
species
Adelie 38.8 46.0
Chinstrap 48.8 58.0
Gentoo 47.5 59.6
Drop rows with missing bill length, then compute mean and maximum bill length per species. Method chaining maps verb-for-verb onto a dplyr pipe:
# A tibble: 3 × 3
species mean max
<fct> <dbl> <dbl>
1 Adelie 38.8 46
2 Chinstrap 48.8 58
3 Gentoo 47.5 59.6
.dropna() ↔︎ filter(!is.na(...)), .groupby() ↔︎ group_by(), .agg() ↔︎ summarise(). Keep the verb dictionary from the slides at hand — most pandas code you’ll meet is combinations of these.
Snippet D — a function with a default argument:
21.6
21.605
[1] 21.6
[1] 21.605
Differences to notice: def + colon + indentation instead of function + braces, ** instead of ^, and an explicit return — Python functions do not auto-return their last expression.
{reticulate}You are given this Python function — save it as forest_helpers.py in your working directory. It builds an (untrained) scikit-learn model:
Random forests are a machine learning technique for predicting a numeric variable (here, body mass in grams) from a set of features (here, bill length, bill depth, and flipper length). They are based on the idea of decision trees.
Start with one decision tree. It predicts by asking a series of yes/no questions about the input — “is flipper length > 200 mm?”, then “is bill length > 45 mm?”, and so on — which sorts the penguins into smaller and smaller groups. A new penguin gets the average body mass of whichever group it ends up in.
A single tree, fit to one dataset, tends to memorise that dataset’s quirks rather than the general pattern — like a student who memorises the practice exam instead of learning the material: great on the exact questions seen before, worse on new ones. This is called overfitting.
A random forest fixes this with a simple trick: build many trees (200, by default here), each one shown only a random subset of the data, then average all of their predictions. Each tree’s mistakes are a little different, so averaging many of them cancels much of the noise out — the forest ends up more accurate and more reliable than any single tree.
Finally, “Regressor” just means the model predicts a number — here, body mass in grams. Its counterpart, a “Classifier” (as in yesterday’s slides), predicts a category instead, like species.
Unlike Exercise 1’s total(), make_forest() doesn’t compute a result itself — it returns a Python object with its own fit() (learn from data) and predict() (make a guess for new data) methods. Working in R:
{reticulate}.source_python("forest_helpers.py").palmerpenguins::penguins into a training and a test set (drop missing values first). We deliberately hide the test set from the model while it trains, then use it afterwards to check the model’s guesses on penguins it has never seen — testing it on the same data it learned from would make it look better than it really is.make_forest(), then call $fit() on the training data and $predict() on the test data — directly on the Python object, from R.library(dplyr)
library(palmerpenguins)
set.seed(13)
data <- penguins |>
select(bill_length_mm, bill_depth_mm, flipper_length_mm, body_mass_g) |>
na.omit()
train_idx <- sample(nrow(data), floor(0.8 * nrow(data)))
train <- data[train_idx, ]
test <- data[-train_idx, ]
model <- make_forest() # untrained sklearn object
invisible(model$fit(train[, 1:3], train$body_mass_g)) # fit — runs in Python
predictions <- model$predict(test[, 1:3]) # predict — also runs in Python
data.frame(actual = test$body_mass_g, predicted = round(predictions)) |> head() actual predicted
1 3450 3763
2 3475 3509
3 3300 3453
4 4200 4225
5 3950 3538
6 3800 3414
[1] 359.3618
model is a live Python object; model$fit(...) and model$predict(...) call its Python methods directly from R. reticulate converts the R data frame slices to the pandas/numpy structures scikit-learn expects, and the predictions back to an R vector — no manual export/import step, and no .py script needs to run standalone.
Two variations if you’re quick:
make_forest(n_estimators = 500L) instead of 200 — note the L for an integer. More trees means more “voters” being averaged; does the RMSE actually improve, or has it already levelled off?lm(body_mass_g ~ ., data = train) in R on the same split, and compare its test-set RMSE to the forest’s. Does the random forest actually predict better here, or is a simple linear model just as good?~10 min
Take a few minutes to add this week’s entry to your reflection log. Commit it together with today’s project work.
Before you leave today, make sure your group has:
source_python()---
title: "Practical #12"
subtitle: "Advanced Statistical Programming using R — Multi-lingual Analysis (Python/R)"
author: "Leonhard Kestel, Lisa Bondo Andersen, Cynthia Huang"
date: "July 10, 2026"
draft: false
format:
html:
theme: default
toc: true
toc-depth: 2
code-tools: true
highlight-style: github
execute:
eval: true
message: false
warning: false
cache: false
---
```{r}
#| include: false
library(reticulate)
use_virtualenv("../.venv", required = TRUE)
```
# Quiz
Before starting, work through this [QUIZ](quiz.qmd){target="_blank"} to check your understanding of this week's lecture on multi-lingual analysis with Python and R.
---
# Overview
Yesterday you saw where R reaches its limits, how Python expresses the same ideas with
different syntax, and how `{reticulate}` runs both languages in one workflow. Today you
practise exactly the three skills from the lecture: **read** Python, **translate** it
to R, and **call** Python from R.
::: {.callout-note}
## Do I need to install Python?
For **Exercise 1.1: no** — you only read Python code and write R.
For **Exercise 1.2** you need a Python that `{reticulate}` can find. The easiest path:
recent versions of reticulate (≥ 1.41) can **provision Python for you** — just install
the R package and it handles the rest on first use:
```r
install.packages("reticulate")
```
If you want a proper Python installation of your own (recommended if you plan to use
Python beyond today):
- [uv](https://docs.astral.sh/uv/) *(recommended)* — fast, modern manager for Python
versions, environments, and packages: `uv venv .venv` creates a project environment,
`uv pip install pandas` adds packages to it
- [Anaconda](https://www.anaconda.com/) *(recommended)* — classic data science
distribution; heavier, but batteries included
- [python.org installer](https://www.python.org/downloads/) *(not recommended)* — no
environment management; global packages get messy quickly
:::
---
# Exercises
## Part 1: Translate Python to R
For each snippet below: first **explain in one sentence what it does** (read it out
loud in your group!), then **write R code that reproduces the shown output**. You don't
need Python for this — the output is already there to check against.
**Snippet A** — a loop and a condition:
```{python}
prices = [4.99, 12.50, 0.80, 7.25]
total = 0
for p in prices:
if p > 1:
total += p
print(round(total, 2))
```
<!-- ::: {.callout-note title="Solution" collapse="true"}
It sums all prices above 1 € — `+=` is compound assignment (`total <- total + p`), and
the indented block is the loop body. Idiomatic R skips the loop entirely:
```{r}
prices <- c(4.99, 12.50, 0.80, 7.25)
sum(prices[prices > 1])
```
A literal translation with `for` and `if` works too, but vectorised subsetting is the
R way — the same logic that makes `numpy` code shorter than plain Python loops.
::: -->
**Snippet B** — a list comprehension over a dictionary:
```{python}
grades = {"anna": 1.3, "ben": 2.7, "carla": 1.0}
passed_well = [name for name, g in grades.items() if g < 2.0]
print(passed_well)
```
<!-- ::: {.callout-note title="Solution" collapse="true"}
`grades` is a dictionary (key → value); the list comprehension collects every key whose
value is below 2.0. R's closest cousin of a dict is a named vector, and the comprehension
becomes a filter on names:
```{r}
grades <- c(anna = 1.3, ben = 2.7, carla = 1.0)
names(grades)[grades < 2.0]
```
Read comprehensions inside-out: `for name, g in grades.items()` iterates over
(key, value) pairs, `if g < 2.0` filters, and the leading `name` says what to keep.
::: -->
**Snippet C** — a `pandas` group-by chain:
```{python}
import pandas as pd
from palmerpenguins import load_penguins # same data as in R
penguins = load_penguins()
result = (penguins
.dropna(subset=["bill_length_mm"])
.groupby("species", observed=True)["bill_length_mm"]
.agg(["mean", "max"])
.round(1)
)
print(result)
```
<!-- ::: {.callout-note title="Solution" collapse="true"}
Drop rows with missing bill length, then compute mean and maximum bill length per
species. Method chaining maps verb-for-verb onto a `dplyr` pipe:
```{r}
library(dplyr)
library(palmerpenguins)
penguins |>
filter(!is.na(bill_length_mm)) |>
group_by(species) |>
summarise(
mean = round(mean(bill_length_mm), 1),
max = round(max(bill_length_mm), 1)
)
```
`.dropna()` ↔ `filter(!is.na(...))`, `.groupby()` ↔ `group_by()`, `.agg()` ↔
`summarise()`. Keep the verb dictionary from the slides at hand — most `pandas` code
you'll meet is combinations of these.
::: -->
**Snippet D** — a function with a default argument:
```{python}
def bmi(weight_kg, height_m, digits=1):
return round(weight_kg / height_m**2, digits)
print(bmi(70, 1.80))
print(bmi(70, 1.80, digits=3))
```
<!-- ::: {.callout-note title="Solution" collapse="true"}
```{r}
bmi <- function(weight_kg, height_m, digits = 1) {
round(weight_kg / height_m^2, digits)
}
bmi(70, 1.80)
bmi(70, 1.80, digits = 3)
```
Differences to notice: `def` + colon + indentation instead of `function` + braces,
`**` instead of `^`, and an explicit `return` — Python functions do **not**
auto-return their last expression.
::: -->
## Part 2: Call Python from R with `{reticulate}`
You are given this Python function — save it as `forest_helpers.py` in your working
directory. It builds an (untrained) `scikit-learn` model:
```{python}
#| echo: true
#| eval: false
from sklearn.ensemble import RandomForestRegressor
def make_forest(n_estimators=200):
"""Create an untrained random forest regressor."""
return RandomForestRegressor(n_estimators=n_estimators, random_state=42)
```
::: {.callout-note title="What does a random forest regressor do?" collapse="true"}
Random forests are a machine learning technique for predicting a **numeric** variable (here, body mass in grams) from a set of **features** (here, bill length, bill depth, and flipper length). They are based on the idea of **decision trees**.
Start with one **decision tree**. It predicts by asking a series of yes/no questions
about the input — "is flipper length > 200 mm?", then "is bill length > 45 mm?", and so
on — which sorts the penguins into smaller and smaller groups. A new penguin gets the
*average* body mass of whichever group it ends up in.
A single tree, fit to one dataset, tends to **memorise that dataset's quirks** rather
than the general pattern — like a student who memorises the practice exam instead of
learning the material: great on the exact questions seen before, worse on new ones.
This is called **overfitting**.
A **random forest** fixes this with a simple trick: build many trees (200, by default
here), each one shown only a random subset of the data, then **average all of their
predictions**. Each tree's mistakes are a little different, so averaging many of them
cancels much of the noise out — the forest ends up more accurate and more reliable than
any single tree.
Finally, "Regressor" just means the model predicts a **number** — here, body mass in
grams. Its counterpart, a "Classifier" (as in yesterday's slides), predicts a
**category** instead, like `species`.
:::
Unlike Exercise 1's `total()`, `make_forest()` doesn't compute a result itself — it
returns a **Python object** with its own `fit()` (learn from data) and `predict()`
(make a guess for new data) methods. Working **in R**:
1. Install and load `{reticulate}`.
2. Load the function with `source_python("forest_helpers.py")`.
3. Split `palmerpenguins::penguins` into a training and a test set (drop missing values
first). We deliberately hide the test set from the model while it trains, then use
it afterwards to check the model's guesses on penguins it has never seen — testing
it on the same data it learned from would make it look better than it really is.
::: {.callout-tip title="Tip" collapse="true"}
The `sample` function is handy for random row selection. For example, to split a data frame `data` into 80% training and 20% test:
```r
train_idx <- sample(nrow(data), floor(0.8 * nrow(data)))
train <- data[train_idx, ]
test <- data[-train_idx, ]
```
:::
4. Create the model with `make_forest()`, then call `$fit()` on the training data and `$predict()` on the test data — directly on the Python object, from R.
<!-- ::: {.callout-note title="Solution" collapse="true"}
```{r}
#| include: false
# Stand-in for source_python("forest_helpers.py"), which is shown but not run:
# define the same function in the embedded Python session and bind it in R.
reticulate::py_run_string("
from sklearn.ensemble import RandomForestRegressor
def make_forest(n_estimators=200):
return RandomForestRegressor(n_estimators=n_estimators, random_state=42)
")
make_forest <- py$make_forest
```
```{r}
#| eval: false
install.packages("reticulate") # once
library(reticulate)
source_python("forest_helpers.py")
```
```{r}
library(dplyr)
library(palmerpenguins)
set.seed(13)
data <- penguins |>
select(bill_length_mm, bill_depth_mm, flipper_length_mm, body_mass_g) |>
na.omit()
train_idx <- sample(nrow(data), floor(0.8 * nrow(data)))
train <- data[train_idx, ]
test <- data[-train_idx, ]
model <- make_forest() # untrained sklearn object
invisible(model$fit(train[, 1:3], train$body_mass_g)) # fit — runs in Python
predictions <- model$predict(test[, 1:3]) # predict — also runs in Python
data.frame(actual = test$body_mass_g, predicted = round(predictions)) |> head()
# RMSE: roughly "how far off is a typical prediction, in grams?" (smaller is better)
sqrt(mean((predictions - test$body_mass_g)^2))
```
`model` is a live Python object; `model$fit(...)` and `model$predict(...)` call its
Python methods directly from R. reticulate converts the R data frame slices to the
pandas/numpy structures `scikit-learn` expects, and the predictions back to an R
vector — no manual export/import step, and no `.py` script needs to run standalone.
::: -->
::: {.callout-tip title="Going further" collapse="true"}
Two variations if you're quick:
- Call `make_forest(n_estimators = 500L)` instead of 200 — note the `L` for an integer.
More trees means more "voters" being averaged; does the RMSE actually improve, or has
it already levelled off?
- Fit a plain `lm(body_mass_g ~ ., data = train)` in R on the same split, and compare
its test-set RMSE to the forest's. Does the random forest actually predict better
here, or is a simple linear model just as good?
:::
---
# Reflection log
*~10 min*
Take a few minutes to add this week's entry to your
[reflection log](_reflection-prompts.qmd). Commit it together with today's project
work.
---
## End-of-practical checklist
Before you leave today, make sure your group has:
- [ ] Read and explained all four Python snippets, and reproduced their output in R
- [ ] Fit a `scikit-learn` random forest from R via `source_python()` and checked its test-set predictions
- [ ] This week's reflection log entry committed and pushed
---
# Resources
- [reticulate documentation](https://rstudio.github.io/reticulate/) — Python in R: setup, conversions, `source_python()`
- [Python for R Users](https://aeturrell.github.io/python4DS/welcome.html) — a full "R to Python" translation guide, written for people who think in R