Working with AI assistants

How to use LLMs on this module without wasting the module

Author

Gabriel Mateus Bernardo Harrington

The short version

You may use AI assistants on this module. There is no ban, and I’m not interested in policing it.

But: the market rate for “can ask a model for R code” is zero, because everyone can do that. Your value — in the rest of this MSc, in your project, and in a job — is in being able to look at generated code and know whether it’s right. That capability is built by writing R, not by reading R.

So the deal is:

ImportantAttempt → ask → diff
  1. Attempt it yourself first.
  2. Ask if you’re stuck.
  3. Diff — compare what you wrote to what you were given, and work out why they differ.

Step 3 is where learning happens and it’s the one everybody skips.

There are two reasons to be careful here, and they are different problems. Assistants are often confidently wrong — and even when they are right, leaning on them costs you the learning. The first is easier to argue; the second does more damage.

Why this matters more than it sounds

Here is R code of the kind an assistant will cheerfully produce. It does not error. It returns numbers of the right shape and a plausible size. It is wrong.

library(tidyverse)

The argument that doesn’t exist

x <- c(1, 2, NA, 4)
mean(x, na.omit = TRUE)
[1] NA

na.omit is not an argument to mean(). It was absorbed by ... and ignored. The argument is na.rm. No warning was issued.

The factor that became its own level codes

counts <- factor(c("10", "20", "5"))
as.numeric(counts)
[1] 1 2 3

1 2 3 — the internal level codes, not your data. You need as.numeric(as.character(counts)). This class of bug has reached publication more than once.

The join that quietly duplicated your samples

samples <- tibble(id = c(1, 2, 3), sample = c("s1", "s2", "s3"))
pheno   <- tibble(id = c(1, 1, 2), measure = c(4.1, 9.9, 5.2))

joined <- left_join(samples, pheno, by = "id")
c(before = nrow(samples), after = nrow(joined))
before  after 
     3      4 

Three rows in, four out, because id == 1 was duplicated in pheno. Every mean you compute downstream is now wrong. Check nrow() before and after every join — an assistant will rarely do this for you.

The filter that silently ate your missing data

df <- tibble(group = c("a", "b", NA, "a"), value = 1:4)
df |> filter(group != "a")
# A tibble: 1 × 2
  group value
  <chr> <int>
1 b         2

One row, not two — NA != "a" evaluates to NA, and filter() keeps only TRUE. If you wanted the missing group kept: filter(group != "a" | is.na(group)).

Note

There are more of these in the session 3 slides. The common thread is that none of them error. Code that crashes is a minor annoyance; code that returns a confident wrong number is how bad science happens — and you cannot spot any of them without understanding factors, NA semantics, and joins.

That understanding is what this module is for.

The second reason, which is the one people miss

Everything above is about the model being wrong. That is the easy argument to make, and it is the one that gets made most often.

The harder argument applies when the model is right.

Reading a correct solution feels almost exactly like working one out. It is quick, it makes sense as you read it, and you finish with the distinct impression that you now know how to do it. That impression is fluency, not learning, and the two feel identical from the inside. The difference shows up later — usually in an exam room, or the first time nobody is there to ask.

Learning scientists call the effort you just skipped a desirable difficulty. Retrieving something yourself, getting it wrong, and correcting it is not an inefficient route to the knowledge. It is the mechanism by which the knowledge forms. Watching a correct answer appear bypasses that mechanism, and it does so silently — nothing about the experience tells you that you have been short-changed.

ImportantThe loop to watch for
  1. You get stuck, and you ask.
  2. You get a working answer, and move on.
  3. You never build the knowledge that answer was made of.
  4. Next time, you are less able to tell a good answer from a bad one.
  5. So you ask sooner.

Each step is individually reasonable. Nothing in the loop feels bad while it is happening. And it feeds directly back into the first problem: the less you know, the less able you are to catch the confident, non-erroring, wrong answer.

None of this is an argument for struggling pointlessly. Being stuck for forty minutes on a missing bracket teaches you nothing. It is an argument for being deliberate about which difficulties you keep — which is what attempt → ask → diff is for.

Which tool for which job

You have access to Gemini, NotebookLM and Elicit. They are not interchangeable, and the difference that matters is how much room each one has to invent.

Tool Grounded in Sensible use on this module
NotebookLM only the sources you upload Revising from your own lecture notes
Elicit real published papers Finding and triaging literature for the report
Gemini nothing in particular General help — every caveat on this page applies

Narrower grounding means less room to invent. It does not mean “safe”.

NotebookLM

NotebookLM answers only from documents you give it, and cites the passage behind each claim so you can click through and check. That makes it the most learning-friendly of the three, because it points you back at the source rather than replacing it.

Genuinely good uses: upload the lecture notes and ask it to explain a concept you did not follow; ask it to generate practice questions from a session; ask what the notes say about a topic before you ask a general assistant to solve it for you.

The catch: grounding reduces invention, it does not eliminate it — it can still misread the source you handed it, and the citation will look just as confident either way. It is also close to useless for “why will this code not run?”, because your error is not in the documents.

Elicit

Elicit searches real papers and can extract findings into a table, which is directly useful for MLO-5 and the report’s literature.

The catch is worth knowing in detail, because it is a reproducibility problem and this module is about reproducible pipelines. A 2026 feasibility study in Research Synthesis Methods re-ran the same extractions from different user accounts and compared them:

  • 90% agreement on the extracted values
  • 46% agreement on the supporting quotes
  • 30% agreement on the reasoning

Elicit’s own documentation notes it “can miss the nuance of a paper or misunderstand what a number refers to”, with complex tables and multi-arm studies especially prone to extraction errors. Independent evaluation has also found a large gap between benchmark screening performance and performance on realistic search strategies.

Use Elicit to find papers, not to know them. If a paper matters enough to cite, it matters enough to open. An extracted summary you cannot reproduce is not evidence, and “Elicit said so” is not a defence of an analytical choice.

Gemini

A general-purpose assistant with no particular grounding, which makes it the most fluent and the most able to invent. Everything on this page — attempt → ask → diff, giving it your real data, constraining the answer, making it argue with itself — applies to it directly.

Tools change faster than this page does. Treat the descriptions above as a starting point and check the current behaviour yourself — which is, conveniently, the exact habit the rest of this page is trying to build.

Tutor mode

The most useful thing you can do is stop your assistant from answering so fast.

There’s a paste-in prompt in r_tutor_prompt.md that reconfigures any assistant into something closer to a good lab demonstrator: it asks what you’ve tried, gives you the smallest useful hint rather than the solution, makes you predict output before showing it, and caps how much code it writes unasked. It has an escape hatch for when you’re genuinely out of time.

Tool-specific versions:

Tool File Where it goes
Any chat assistant r_tutor_prompt.md Custom instructions / project instructions / first message
Claude Code skills/r-tutor/SKILL.md ~/.claude/skills/r-tutor/SKILL.md
A project folder CLAUDE.md.example Save as CLAUDE.md in your coursework folder

You can defeat all of this by opening a different tab, and I know that. It isn’t a fence. It’s there for the days when you’d rather learn the thing.

Prompting for R, specifically

1. Give it your data, not your description of your data

Most bad generated R comes from the model guessing at your data’s shape. Don’t make it guess:

glimpse(df)
str(df)
dput(head(df, 5))   # a reproducible copy of your data - paste this

Then add what str() can’t show:

“A row is one participant-visit, so IDs repeat. measure is mg/dL. -99 means missing in age.”

That one sentence prevents the duplicated-join bug above.

Better still, let R do it. The btw package hands your live session — real data frames, real package versions, real help pages — to an assistant:

install.packages("btw")
library(btw)
btw()

Also worth your time: ellmer for calling models from R, and the assistant built into Positron.

2. Constrain the answer

Vague question, average answer.

NoteWeak

“How do I summarise this data?”

TipBetter

“Using dplyr only, no new packages: mean and SD of measure by treatment_group. Keep groups with fewer than 5 observations visible rather than dropping them. Tell me what happens to NAs in measure.”

The second names the tools, the output and the edge case you’re worried about. Being precise about edge cases is most of the skill.

3. Ask it to argue with itself

Models are much better at criticising code than at writing it correctly first time. Exploit that:

  • “What assumptions did you make about my data?”
  • “How could this fail? Give me three ways.”
  • “What would you check to prove this worked?”
  • “Is there a tidyverse function that already does this?”
  • “You used sapply() — what happens if the results have different lengths?”

4. Trust the docs over the model

If an assistant tells you an argument exists, ?function_name will tell you whether it does. That check takes three seconds and models hallucinate tidyverse arguments constantly.

5. Notice what it doesn’t do

Assistants almost never volunteer: row counts around joins, NA handling, checking whether a column is really the type you think, plotting the data before modelling it. Those omissions are yours to catch.

Optional: wiring an assistant into your R session

WarningLeave this one until later

Everything in this section is optional and unsupported. I can’t debug it for you in a lab session, and on a managed university laptop it may not be permitted at all. Come back to it once you can write R unaided. It’s documented here because it exists and you’d find it anyway — not because you need it to pass.

Look again at the four broken examples near the top of this page. They fail for one shared reason: the model was guessing at your data. It had no way to know that pheno contained a duplicated id, that group held an NA, or that counts was a factor rather than a number.

You can’t prompt your way out of that. The structural fix is to stop making it guess.

Level 1 — btw

The lower-friction option, and the one to try first. btw collects context from your live R session — the actual structure of your data frames, your installed package versions, help pages — and puts it on the clipboard for you to paste into any assistant.

install.packages("btw")
library(btw)

btw()          # describes your session and environment
btw(pheno)     # describes one specific object

No configuration, no editing JSON files, and it works with a plain chat window. For most people this is the whole benefit at a fraction of the effort.

Level 2 — mcptools

mcptools goes further: it lets an assistant run code in your live R session, so it can inspect objects and read help pages itself rather than being told about them.

install.packages("mcptools")

Then register your R session so an assistant can find it. Adding it to your .Rprofile means every session registers automatically:

usethis::edit_r_profile()   # then add the line below to that file
mcptools::mcp_session()

Finally, point your assistant at R. For Claude Code, one command:

claude mcp add -s "user" r-mcptools -- Rscript -e "mcptools::mcp_server()"

For Claude Desktop and most other MCP clients, add this to the client’s config file instead:

{
  "mcpServers": {
    "r-mcptools": {
      "command": "Rscript",
      "args": ["-e", "mcptools::mcp_server()"]
    }
  }
}

Restart both R and the assistant afterwards.

The catch, which is real

Two things to be honest about.

This makes it much easier to skip step 1. An assistant that can see your data and run your code is a very short path from “I have a problem” to “I have an answer”, and attempt → ask → diff only works if you actually attempt. Pair this with tutor mode or you have simply built yourself a faster way to not learn R.

A better-informed assistant is more convincingly wrong. Session access removes one whole category of error — the invented column, the misjudged data shape. It removes none of the others. It will not tell you that a t-test was the wrong choice, that your groups aren’t independent, or that the effect you found is an artefact of how you filtered. Those are the judgements the report is marked on, and they are still entirely yours.

What’s fine and what isn’t

Fine — genuinely, use the tools:

  • Explaining an error you can’t parse
  • “What does this function do?”
  • Debugging code you wrote
  • Sanity-checking your interpretation of a result
  • Improving your writing
  • Finding out what packages exist for a problem

Not fine in assessed work:

  • Generating the analysis code you submit
  • Having it choose your statistical approach
  • Having it write your interpretation

This isn’t about purity. The 2000-word report is explicitly marked on defending your analytical choices and methodological approach (MLO-5). You cannot defend a choice you didn’t make, and the attempt is transparent from the outside.

If you’re unsure whether something crosses a line, ask me. Asking is never the wrong move.

Declaring what you used

For the summative report, you’ll be asked for a short process appendix — a few hundred words, unmarked for style:

  • what you used an assistant for
  • one thing it got wrong or that you rejected, and how you spotted it
  • what you changed and why

This is not a confession. Honest, specific answers here demonstrate exactly the evaluative skill the module is teaching, and “I used it to debug my pivot_longer() call and it suggested a column that didn’t exist” is a better answer than “I didn’t use it”.

Further reading

  • R for Data Science (2e) — still the best answer to most R questions
  • btw — give an assistant your R session
  • ellmer — LLMs from R
  • mcptools — let an assistant run code in your live R session (optional, see above)
  • ?function_name — faster than asking, and always correct