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

Practical #3

Advanced Statistical Programming using R - Debugging

Author

Leonhard Kestel, Lisa Bondo Andersen, Cynthia Huang

Published

April 30, 2026

Quiz

Before starting, work through this QUIZ to check your understanding of the concepts covered in this week’s lecture on debugging and on using LLMs (large language models) in a statistical programming workflow.

General Remarks

Two practicals in, you now have a small but growing code base of things that can — and will — break. This session is about what to do when they do, plus your first hands-on session with an LLM assistant as part of the workflow.

ImportantA note on rAI learning space by aihorizon R&D.

We’ll be using the rAI learning space by aihorizon R&D in the course — a web-based platform that gives you access to several state-of-the-art LLMs through one interface, including OpenAI’s GPT family (GPT-5.2, GPT-4o, o3-mini), Microsoft’s Phi and MAI models, and locally-hosted models. You can pick the model that fits each task.

  • We have arranged a free premium subscription for the class at least until the end of the semester. That’s free access to state-of-the-art models that would otherwise cost around €20/month each. Use it for this course, for other courses, or for personal projects — it’s yours to use however you like.
  • Using it is optional, but strongly suggested. We’ll use it in the lectures and practicals moving forwards.
  • There is a consent form and an intro survey in the platform when you first log in. Please read it before ticking through and complete the survey before using the platform.
  • If you’ve never used one of these tools before, don’t worry: we’ll walk you through the setup step by step, and most of the practical today is about learning to use it well for statistical programming.

Exercise 0: Set up the rAI learning space by aihorizon R&D.

It’s the first time you’re using the rAI learning space, and the habits you form here (system prompt, how you prompt, how you read output) will shape how useful the tool is to you for the rest of the course.

0.1 Log in, consent, and survey

  1. Navigate to the rAI learning space platform (https://polite-wave-029999803.7.azurestaticapps.net/) and log in with the credentials you were sent earlier today via email. Your account comes with a premium subscription for the duration of the course.

  2. On first login you will see a consent form. Read it carefully before continuing.

  3. Complete the intro survey on the platform.

0.2 Configure your system prompt

  1. In the platform’s Model Settings (on the left), find the System prompt field. A system prompt is a standing instruction that the model sees before every message you send — think of it as persistent preferences, not a one-off request.

  2. Start from the template below and edit it to reflect how you actually work. At minimum, change the code style section to match your own preferences (e.g. %>% vs |>, tidyverse vs base vs data.table). If you have package preferences from Practical #2 (e.g. here for paths, readr over base read.csv), put them here.

TipExample system prompt (starting point — edit it)
You are helping me with an advanced statistical programming course in R.

Code style:
- Use the tidyverse (dplyr, tidyr, ggplot2, purrr) where possible.
- Use the native pipe |> rather than %>%.
- Follow the tidyverse style guide: snake_case, <- for assignment, two-space indent.
- Prefer small, pure functions over long scripts.
- For file paths use the `here` package.

When I ask for help debugging:
- First explain what the error message means, in plain English.
- Then suggest where to look in my code, BEFORE proposing a fix.
- Only write a full rewrite if I ask for one.

General:
- Do not apologise. Do not pad with filler. If you are uncertain, say so.
- If I paste in code, assume it is real code I care about — do not silently
  rename variables or change the pipe style.
  1. Save the system prompt by clicking Model Settings again. Be aware that the System Prompt (or any of the other model settings) don’t save when you reload or close the page, so make sure to copy and paste it somewhere else.
  2. BONUS: Find out what it means to change the temperature value (or any of the other model settings). Where would you like to put the temperature when e.g. writing code, proof-reading an email, creating a game for your next meeting with friends…?

0.3 Calibrate: does it follow your prompt?

  1. Start a new conversation. Ask the model to generate a small function — something like:

    Write an R function that takes a numeric vector and returns a tibble with the mean, median, and standard deviation. Include one worked example.

  2. Read the reply carefully. Does it follow your style preferences? (Pipe style, naming, assignment operator.) If not, tweak your system prompt and try again. This calibration loop is important — a system prompt that the model ignores is worse than no system prompt, because it gives you false confidence.

  3. When you’re satisfied with the output, save the session.

0.4 Revisit a previous conversation

  1. Reload your page and navigate to the History tab (on the left)

  2. Locate your previous session and click the Duplicate icon (next to the trash bin). This restores your conversation and you can continue writing. Note that the Model settings are still set to default, so these need to be updated manually.

0.5 A ground rule for the rest of the session

  1. For every LLM interaction you have today, follow this protocol:

    • Read the code yourself first (at least 30 seconds).
    • Write down what you think is wrong before you prompt.
    • Ask for an explanation, not a rewrite on the first turn.

This is not busywork — it’s the difference between using the LLM as a tool and being used by it.

Exercise 1: Using the LLM deliberately

The goal here is to practice how to prompt, not just whether to prompt. Work with the system prompt you configured in Exercise 0 and follow the protocol from 0.5.

  1. Copy the following buggy snippet into a new R script. Do not run it yet.
library(palmerpenguins)
library(dplyr)

mean_mass_by <- function(data, group_var) {
  data |>
    group_by(group_var) |>
    summarise(mean_mass = mean(body_mass_g, na.rm = TRUE))
}

mean_mass_by(penguins, species)
  1. Before asking the LLM anything, read the code carefully and write down (on paper or in a comment) what you expect it to do and what you think might go wrong. This 30-second pause is the single most effective debugging habit you can build.

  2. Now run the code. Copy the full error message.

  3. Go back to the platform, start a new conversation and check that your model settings are set as you want them to be. Ask the LLM to explain the error message, not to fix it. A good prompt template:

    Here is my code: [code]. Here is the error: [error]. Please explain what R is complaining about, and point me to the line where the problem originates. Do not rewrite my code yet.

  4. Once you understand why it fails, fix the function yourself. Then ask the LLM to review your fix.

NoteSolution

This is a non-standard evaluation (NSE) bug. Inside the function, group_var is the literal string "group_var" — not the column species you passed in. Fix with the embrace operator { }:

library(dplyr)

mean_mass_by <- function(data, group_var) {
  data |>
    group_by({{ group_var }}) |>
    summarise(mean_mass = mean(body_mass_g, na.rm = TRUE))
}
mean_mass_by(penguins, species)
  1. Reflect for a minute with your neighbour: what did the LLM get right? Did it hallucinate anything? Would you have found the bug faster without it?

Exercise 2: Locating errors in R code

When R throws an error, the message alone often isn’t enough — you need to know where in the call stack it came from.

  1. Copy the following into a new R script and run it. You should get an error.

::: {.cell}

library(palmerpenguins)
library(dplyr)
 
summarise_species <- function(data) {
  data |>
    group_by(species) |>
    summarise(mean_mass = mean_body_mass(body_mass_g))
}
 
mean_body_mass <- function(x) {
  mean(x, na.rm = TREU)   
}
 
summarise_species(penguins)

:::

  1. Read the error message first. dplyr errors are short but structured — each line tells you something different. Before you touch anything else, work through it line by line: where is the error, and what is it?

  2. Now run rlang::last_trace() (or traceback() if you prefer the older format). You will see a long stack — most of it is dplyr internals you can ignore. Scan bottom-up for frames that name your own functions. Which is the innermost frame that belongs to code you wrote?

NoteSolution

Read the error message line by line:

Error in `summarise()`:
ℹ In argument: `mean_mass = mean_body_mass(body_mass_g)`.
ℹ In group 1: `species = Adelie`.
Caused by error in `mean_body_mass()`:
! object 'TREU' not found
Run `rlang::last_trace()` to see where the error occurred.

Each line adds one piece of information:

  • Error in summarise() — the dplyr verb where it surfaced.
  • In argument: ... — which argument of summarise() was being computed.
  • In group 1: species = Adelie — which group was being processed.
  • Caused by error in mean_body_mass() — your helper function.
  • object 'TREU' not found — the actual R-level problem.
  • Run rlang::last_trace() ... — dplyr tells you what to do next.

You already know the bug is in mean_body_mass() before running last_trace(). The backtrace just confirms the exact line.

The backtrace. rlang::last_trace() shows ~14 frames. Most are dplyr internals (summarise.grouped_df, summarise_cols, map, lapply, mask$eval_all_summarise, …) — safe to skip. The frames that matter are:

  • Frame 1: summarise_species(penguins) — your top-level call.
  • Frame 11: mean_body_mass(body_mass_g) — your function, innermost. This is where the bug lives.
  • Frames 12–14: mean() → mean.default() → isTRUE(na.rm) — base R tries to evaluate TREU and can’t find it.

Fix: replace TREU with TRUE on the na.rm line of mean_body_mass().

Lesson: the error message usually gets you close; the backtrace pins down the line. Scan for your own function names and stop at the innermost one.

  1. Replace TREU with a value that does exist but makes no sense, e.g. na.rm = "banana". Re-run. How does the error message change? This is a classic “the error is not where you think” situation — the traceback still points to mean_body_mass, but the underlying message is now about argument types, coming from deeper inside mean.default().

  2. Fix both bugs.

Exercise 3: browser()

browser() drops you into an interactive session inside a running function, so you can inspect variables at the moment things go wrong. The six commands you need are:

Command Effect
n Run the next line
s Step into a function call on the current line
f Finish the current loop / function
c Continue until the next browser() or the end
Q Quit the debugger
where Print the call stack
  1. Consider this buggy recursive factorial:
my_factorial <- function(n) {
  if (n == 1) return(1)
  return(n * my_factorial(n - 1))
}

my_factorial(5)   # returns 120 — correct
my_factorial(0)   # hangs / errors
my_factorial(3.5) # also wrong
  1. Insert browser() as the first line of the function body and call my_factorial(0). Use n to step through. At each step, check the value of n. What is happening?
NoteSolution

The base case is n == 1, but with n = 0 we never hit it — we recurse to -1, -2, … and either blow the stack or (with 3.5) never reach an integer. Fix by broadening the base case:

my_factorial <- function(n) {
  stopifnot(n >= 0, n == as.integer(n))
  if (n <= 1) return(1)
  return(n * my_factorial(n - 1))
}

Note that inserting a stopifnot() upfront is a debugging-prevention tool: it fails loudly on bad input instead of hanging silently.

  1. Remove the browser() call once you have fixed the function.

Exercise 4: debug() and debugonce()

browser() requires editing the function body, which is inconvenient when the function lives in a package. debug() and debugonce() attach a debugger to a function from the outside.

  • debug(f) — enter the debugger on every subsequent call to f(), until you run undebug(f).
  • debugonce(f) — enter the debugger on the next call to f(), then detach automatically.
  1. Take the following (silently) buggy function:
standardise <- function(x) {
  (x - mean(x)) / sd(x)
}

standardise(c(1, 2, 3, 4, 5))   # fine
standardise(c(1, 2, NA, 4, 5))  # returns all NAs — why?
  1. Run debugonce(standardise) and then standardise(c(1, 2, NA, 4, 5)). Step through with n. At each line, print x, mean(x), and sd(x). Which value is the NA coming from?
NoteSolution

mean(x) and sd(x) both return NA by default when x contains NA, which propagates to the result. Fix with na.rm = TRUE:

standardise <- function(x) {
  (x - mean(x, na.rm = TRUE)) / sd(x, na.rm = TRUE)
}

Note that debugonce() detaches itself after one call — try running standardise() again and you will not be dropped into the debugger. Compare with debug(standardise), which would keep re-entering until you call undebug(standardise).

  1. When would you prefer debug() over debugonce()? Jot down one scenario.

Exercise 5: Debugging Quarto

Not every error comes from R. When quarto render fails, the first job is to work out which tool is complaining: R (your code), knitr (the engine that runs your code), Pandoc (the renderer), or LaTeX (only for PDF output). The console output usually tells you, but you have to read it carefully.

  1. Create a new file broken.qmd in your course folder and paste in:

    First chunk

        library(palmerpenguins)
        head(penguins)

    Second chunk

        penguins |>
          filter(species = "Adelie") |>
          ggplot(aes(x = bill_length_mm, y = bill_depth_mm)) +
          geom_point()
  2. From the command line, run quarto render broken.qmd. You should see it fail. Before fixing anything, answer: does the error come from R, from knitr, or from Quarto/Pandoc? How can you tell?

NoteSolution

There are three separate problems, each surfacing from a different layer of the stack. Chunks knit top-to-bottom, so you will encounter them in this order:

  1. R code error in the second chunk: filter(species = "Adelie") uses = (argument assignment) instead of == (equality). dplyr raises an error like “Problem while computing ..1 = species = \"Adelie\"”. Fix: filter(species == "Adelie").
  2. Missing library (still second chunk, revealed after you fix bug 1): ggplot2 is not loaded, so ggplot() is not found. Add library(ggplot2) (or library(tidyverse)).
  3. Knitr chunk-option error in the third chunk: echo=TREU is evaluated by knitr before the chunk body runs. TREU is not a defined object, so knitr aborts with object 'TREU' not found. Fix: echo=TRUE.

The pedagogical point: fix one error, re-render, read the next error. The error message changes layer each time — a dplyr runtime error, then a namespace lookup error, then a knitr option-parse error. Learning to read which tool is complaining is half the skill.

  1. Fix the errors one at a time, re-rendering after each fix. Notice how the error message changes as you peel back the layers.

  2. Change format: html to format: pdf and try to render again. If you do not have a LaTeX distribution installed you will get a different class of error entirely, from LaTeX. Install tinytex::install_tinytex() only if you want to explore this; otherwise revert to html.

Exercise 6: Reflection Log

  1. Take some time to write this week’s reflection log.
  2. Add and commit your reflection log to your Git repository (the one we initialized last week).

Exercise 7 (Optional): Debugging with Regular Expressions

Regular expressions are powerful tools for finding and fixing patterns in text, code, and data. In this exercise, you’ll use regex to locate bugs, clean messy data, and validate inputs - common real-world debugging scenarios.

7.1 Finding bugs in code with regex

You’ve inherited a messy R script with several common coding errors. Use regex to systematically find and fix them.

  1. Create a new file messy_analysis.R and paste in this buggy code:
    library(palmerpenguins)
    library(dplyr)
    
    # Some analysis functions with bugs
    calculate_mean<-function(data,col_name){
      result=data%>%summarise(mean_val=mean(col_name,na.rm=T))
      return result
    }
    
    clean_species_name <- function(species_text) {
      # Remove extra whitespace and fix common typos  
      species_text %>% 
        str_replace_all("  +", " ") %>%
        str_replace_all("adelie", "Adelie") %>%
        str_replace_all("chinstrap", "Chinstrap") 
    }
    
    penguin_summary<-penguins%>%
      filter(species="Adelie")%>%
      mutate(bill_ratio=bill_length_mm/bill_depth_mm)%>%
      summarise(
        mean_ratio=mean(bill_ratio,na.rm=TRUE),
        median_ratio=median(bill_ratio,na.rm=TRUE)
      )
  1. Use regex patterns to find these common bugs:
    • Missing spaces around operators: Find lines with <-, =, %>% that lack proper spacing
    • Single = in filter conditions: Find filter() calls using = instead of ==
    • Missing return() parentheses: Find return statements without parentheses
  2. In RStudio, use Find & Replace with regex enabled:
    • Pattern: (\w)<-(\w) → Replacement: $1 <- $2 (add spaces around assignment)
    • Pattern: filter\(([^=]+)=([^=]+)\) → Replacement: filter($1 == $2) (fix filter equality)
    • Pattern: return (\w+)$ → Replacement: return($1) (add return parentheses)

7.2 Cleaning messy survey data

Real data often contains inconsistent formatting. Use regex to standardize penguin species names in a messy dataset.

  1. Create this messy data:
    library(stringr)
    
    messy_survey <- tibble(
      id = 1:8,
      species_reported = c(
        "adelie penguin", 
        "  Adelie  ",
        "ADELIE",
        "Chinstrap penguin",
        "chin strap", 
        "Gentoo",
        "gentoo penguin  ",
        "adelie"
      ),
      body_mass = c(3750, 3800, 3900, 3733, 3950, 5076, 5000, 3625)
    )
  1. Write regex patterns to standardize the species names to exactly: “Adelie”, “Chinstrap”, “Gentoo”:
    clean_survey <- messy_survey %>%
      mutate(
        species_clean = species_reported %>%
          str_trim() %>%  # Remove leading/trailing whitespace
          str_to_title() %>%  # Proper case
          str_replace_all("\\s*Penguin\\s*", "") %>%  # Remove "penguin"
          str_replace_all("Chin\\s*Strap", "Chinstrap") %>%  # Fix chinstrap variants
          str_extract("Adelie|Chinstrap|Gentoo")  # Extract valid species only
      )
  1. Validate your cleaning worked by checking for any remaining invalid entries:
    clean_survey %>% 
      filter(is.na(species_clean) | 
             !species_clean %in% c("Adelie", "Chinstrap", "Gentoo"))

7.3 Validating function inputs

Write a function that uses regex to validate penguin measurement inputs before analysis.

  1. Create a validation function:
    validate_measurement <- function(measurement_string) {
      # Valid format: number (integer or decimal) followed by "mm" 
      # Examples: "39.1mm", "42mm", "18.7mm"
      
      valid_pattern <- "^\\d+(\\.\\d+)?mm$"
      
      if (str_detect(measurement_string, valid_pattern)) {
        # Extract numeric value
        numeric_value <- str_extract(measurement_string, "\\d+(\\.\\d+)?") %>% 
          as.numeric()
        return(list(valid = TRUE, value = numeric_value))
      } else {
        return(list(valid = FALSE, value = NA, 
                   error = paste("Invalid format:", measurement_string)))
      }
    }
    
    # Test cases
    test_inputs <- c("39.1mm", "42mm", "18.7mm", "39.1", "42 mm", "abc", "39.1cm")
    
    map(test_inputs, validate_measurement)
  1. Debug challenge: The function above has a subtle bug. Test it with edge cases like "0mm", "999.999mm", and ".5mm". Can you spot and fix the issue?

7.4 LLM interaction: Explaining regex patterns

  1. Go back to your rAI platform and start a new conversation.

  2. Ask the LLM to explain what this complex regex pattern does:

1+(?:+[A-Za-z]+)*(?:penguin|Penguin)?$

  1. Follow up by asking it to help you write a regex pattern for a specific task: > “Write a regex pattern that matches valid R variable names: starts with letter or dot, can contain letters, numbers, dots, underscores, but cannot start with dot followed by number.”

  2. Test the LLM’s suggested pattern against these test cases:

    test_vars <- c("x", "data1", ".hidden", ".2bad", "my_var", "var.name", "123bad", "_underscore")

Reflection: Did the LLM explain the regex clearly? Did its suggested pattern work correctly on all test cases?

References

  • Shannon Pileggi, Debugging in R (NHS-R 2023 workshop) — slides this lecture drew on.
  • Jenny Bryan & Jim Hester, What They Forgot to Teach You About R, Chapter 11: Debugging R code.
  • rstats-wtf/wtf-debugging — worked examples, many adapted in this practical.
  • Hadley Wickham, R for Data Science (2e), Chapter 5: Workflow — getting help.

Footnotes

  1. A-Za-z↩︎