StatProg2
  • Home
  • Syllabus
  • Group Project
  • Reflection Prompts
  • Setup

On this page

  • Quiz
  • Overview
  • Exercises
    • Part 1: Translate Python to R
    • Part 2: Call Python from R with {reticulate}
  • Reflection log
    • End-of-practical checklist
  • Resources

Practical #12

Advanced Statistical Programming using R — Multi-lingual Analysis (Python/R)

Author

Leonhard Kestel, Lisa Bondo Andersen, Cynthia Huang

Published

July 10, 2026

Quiz

Before starting, work through this QUIZ 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.

NoteDo 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:

install.packages("reticulate")

If you want a proper Python installation of your own (recommended if you plan to use Python beyond today):

  • 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 (recommended) — classic data science distribution; heavier, but batteries included
  • python.org installer (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:

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
NoteSolution

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:

prices <- c(4.99, 12.50, 0.80, 7.25)
sum(prices[prices > 1])
[1] 24.74

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:

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)
['anna', 'carla']
NoteSolution

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:

grades <- c(anna = 1.3, ben = 2.7, carla = 1.0)
names(grades)[grades < 2.0]
[1] "anna"  "carla"

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:

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)
           mean   max
species              
Adelie     38.8  46.0
Chinstrap  48.8  58.0
Gentoo     47.5  59.6
NoteSolution

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:

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)
  )
# 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:

def bmi(weight_kg, height_m, digits=1):
    return round(weight_kg / height_m**2, digits)

print(bmi(70, 1.80))
21.6
print(bmi(70, 1.80, digits=3))
21.605
NoteSolution
bmi <- function(weight_kg, height_m, digits = 1) {
  round(weight_kg / height_m^2, digits)
}

bmi(70, 1.80)
[1] 21.6
bmi(70, 1.80, digits = 3)
[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.

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:

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)
NoteWhat does a random forest regressor do?

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.
TipTip

The sample function is handy for random row selection. For example, to split a data frame data into 80% training and 20% test:

train_idx <- sample(nrow(data), floor(0.8 * nrow(data)))
train <- data[train_idx, ]
test  <- data[-train_idx, ]
  1. 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.
NoteSolution
install.packages("reticulate")   # once
library(reticulate)
source_python("forest_helpers.py")
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
# RMSE: roughly "how far off is a typical prediction, in grams?" (smaller is better)
sqrt(mean((predictions - test$body_mass_g)^2))
[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.

TipGoing further

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. Commit it together with today’s project work.


End-of-practical checklist

Before you leave today, make sure your group has:


Resources

  • reticulate documentation — Python in R: setup, conversions, source_python()
  • Python for R Users — a full “R to Python” translation guide, written for people who think in R
Source Code
---
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