AI for Data Analysis and Coding in R
Directed Studies on Artificial Intelligence in Health Sciences
Learning objectives for this lesson:
- Describe the main forms of AI coding assistance (chat assistants, editor assistants, coding agents and code-executing data tools) and what each can see and do.
- Summarize the evidence from Peng and colleagues (2023) and METR (2025) on AI and programming speed, and explain why the gains depend on the task and on the user's ability to judge the code.
- Describe a dataset to an AI assistant through its data dictionary and request R code in small steps that are run and read one at a time.
- Read basic R code well enough to explain what each line does, and use an assistant to explain unfamiliar functions and error messages.
- Check an AI-written analysis by confirming row counts and missing values, recomputing results by a second method, plotting the data and testing code on data with a known answer.
- Diagnose and correct a missing-value code counted as a number and an age grouping with the wrong boundaries in AI-proposed R code.
- Explain the garden of forking paths and use a pre-specified analysis plan to guard against selective reporting.
- Explain why identifiable or confidential health data must not be uploaded to consumer AI services, and describe the conditions that platforms such as Population Data BC attach to data access.
- Obtain AI help with code for confidential data through data dictionaries, synthetic data and local models, and apply the OCAP® principles to AI use with First Nations data.
This course was developed by Dr. Kiffer G. Card, Faculty of Health Sciences, Simon Fraser University. Product examples are current as of 2026.
AI Coding Assistants: Forms, Evidence and Limits
Learning Objectives for this section
- Describe four forms of AI coding assistance (chat assistants, assistants built into code editors, coding agents and code-executing data tools) and state what each one can see and do.
- Summarize the controlled experiment by Peng and colleagues (2023) and the randomized trial by METR (2025), including the setting, the participants and the main result of each.
- Explain why the gains from AI coding tools depend on the task and on the user's ability to judge the code.
- Identify the R tasks on which an assistant helps a beginner most and the decisions that remain with the analyst.
- Describe the Cedar Valley analysis that this lesson follows and the rules under which it is carried out.
Most quantitative work in public health is now done by writing code. An analyst who estimates the prevalence of loneliness, compares vaccination coverage across regions or fits a regression model usually does so in a statistical programming language such as R, Python, SAS or Stata. Until recently, a student who had never programmed needed weeks of practice before producing a first reliable table. AI coding assistants change that starting point. A student can describe an analysis in plain language, receive working code within seconds, ask what each line does and ask for help when R reports an error. This lesson teaches the practices that turn that capability into analyses a student can trust and defend.
The lesson follows a single analysis from beginning to end. It uses the 2021 wave of the Canadian Social Connection Survey, a public, de-identified dataset that appears in HSCI 207 Lesson 11 and in several HSCI 410 lessons, and it shows the prompts, the code an assistant proposed, the real R output and the checks applied at each step. This first section describes the tools and summarizes what controlled studies show about their effect on the speed of programming work.
Box 8.1 introduces the Cedar Valley Health Authority, its Connector program for socially isolated and lonely older adults, and Jordan, a co-op student whose first analysis in R supplies the example followed through the lesson.
The Cedar Valley Health Authority is a fictional, mostly rural health authority in British Columbia with about 210,000 residents, about 46,000 of them aged 65 and older, and 24 primary care clinics. Its Cedar Valley Connector program links adults aged 65 and older who screen as socially isolated or lonely with community groups, volunteer roles, transportation help and services. The program screens with the three-item UCLA Loneliness Scale, on which scores run from 3 to 9, and a score of 6 or higher triggers a referral.
Jordan, a Health Sciences co-op student on a four-month placement with the population health team, is asked by the team lead for some national context before a planning meeting: in a large Canadian survey, how common is a score of 6 or higher among adults aged 65 and older compared with younger adults? Jordan has never written R. The team is piloting an enterprise AI assistant that the health authority procured after a privacy impact assessment; under the pilot's rules it may be used with public documents, internal non-confidential documents and aggregate data, and it must never receive personal health information. Jordan records each prompt in the pilot's AI use log, and the team's epidemiologist has agreed to review the finished script.
Why analysts write code
A spreadsheet hides its steps: once a column has been recoded or a row deleted by hand, nobody can see what was done. A script, which is a plain text file of instructions that a program carries out in order, records every step. Anyone with the script and the data can run it again, see each decision and obtain the same result. That property is called reproducibility, and it is the reason HSCI 410 Lesson 1 treats the script as the official record of an analysis.
R is a free, open-source language for statistics and graphics. It was created by the statisticians Ross Ihaka and Robert Gentleman at the University of Auckland in the early 1990s and is now maintained by a large international community. Much of its usefulness comes from packages, which are bundles of additional functions that users install. This lesson uses three of them: dplyr, for selecting, filtering and summarizing data; readr, for reading and writing data files; and ggplot2, for plots. Hadley Wickham, chief scientist at Posit, created both dplyr and ggplot2.
Language models write code well for reasons that Lesson 2 explained. Their training data contain very large amounts of public code, and code is highly regular, so the next token in a line of R is often easy to predict. Code also has a property that ordinary prose lacks: it can be run. A factual claim in a paragraph can only be checked against a source, but a line of R either runs or produces an error message, and when it runs, its result can be compared with a count, a hand calculation or a second method. AI-written code is therefore easier to verify than many other kinds of AI output, provided that the user actually runs and checks it.
The next part describes the four forms in which AI coding assistance is available and what each can see of the data.
Four forms of coding assistance
Coding assistance arrives in four main forms in 2026. They differ in what the assistant can see, in who runs the code and in how much the assistant does before a person reviews its work. The products named below are examples available in 2026; the categories are likely to outlast any particular product.
The tabs that follow describe the four forms in turn, with examples of current products and the main weakness of each.
Chat assistants are the general-purpose assistants that most students already use, such as ChatGPT, Claude, Gemini and Microsoft Copilot. The user describes a task in a conversation, pastes in code or an error message, and copies the code that comes back into R. The assistant sees only what the user types or pastes. This form suits learning well, because every exchange is visible and the user runs each piece of code personally. Its main weakness is that the assistant has never seen the data, so it guesses variable names, types and codes unless the user supplies them.
Assistants built into code editors work inside the program where code is written. GitHub Copilot suggests the next lines of code as the user types, in editors that support it. Posit, the company that develops the RStudio and Positron editors for R, introduced Posit Assistant in 2026, a conversational assistant that can read the analyst's live R session. Because these assistants see the open script and sometimes the data in memory, their suggestions fit the analysis more closely. Their weakness is speed: a suggestion appears in grey and is accepted with one key, which makes it easy to accept code without reading it.
Coding agents, such as Claude Code and OpenAI's Codex, carry out a multi-step task with less supervision. Following the agent loop described in Lesson 2, the agent plans the task, edits files across a project, runs the code, reads the results and repeats until it judges the task complete. Agents suit larger projects and repetitive changes, for example updating twenty scripts to use a renamed variable. They also make many changes before a person looks at them, and they can run commands on the user's computer, so the review points that Lesson 2 recommended matter more than they do in a chat.
Code-executing data tools write and run code in a protected environment on files that the user uploads, and they return tables, charts and answers directly. The data analysis feature of ChatGPT, for example, runs Python code on uploaded spreadsheets. These tools give a fast first look at a dataset. They have two drawbacks for health research: the data leave the user's computer, which Section 4 shows is often not permitted, and the code that produced an answer stays hidden unless the user asks to see it, so the result cannot be checked or reproduced without extra effort.
Table 8.1 sets the four forms side by side, with what each can see, who runs the code and the work each suits.
Table 8.1. Four forms of AI coding assistance, with what each can see, who runs the code and the work each suits.
| Form | What it can see | Who runs the code | Suited to |
|---|---|---|---|
| Chat assistant | Only what the user pastes | The user | Learning, small steps, explanations of code and errors |
| Editor assistant | Open files and, for some tools, the live R session | The user | Writing code in context and fixing errors in place |
| Coding agent | The whole project folder | The agent, with user approval | Multi-file changes and repetitive edits |
| Code-executing data tool | The uploaded data file | The tool, out of sight | A quick first look at non-confidential data |
What the assistant knows about the data
All four forms share one limit: an assistant knows only what it has been told or shown about the data. A chat assistant that has never seen the Canadian Social Connection Survey cannot know that the age variable is named DEMO_age, that the loneliness items are stored as ordered categories, or that a question shown to a respondent and left blank is recorded as the text "Presented but no response". It fills each gap with a plausible guess, and plausible guesses produce code that looks right. There are two ways to close the gap. The user can describe the data through its data dictionary, which is the document that lists each variable's name, meaning, type, permitted values and missing-value codes (Section 2 shows how), or the user can give a tool access to the data itself, which is subject to the governance rules in Section 4.
With the forms of assistance and their shared limit described, the next part turns to the evidence from randomized studies on whether AI tools make programmers faster.
What the evidence says about speed
Two randomized studies are often cited on the question of whether AI tools make programmers faster. They reached very different results, and the reasons for the difference are the most useful part of the evidence.
A defined task completed 56 percent faster
Peng, Kalliamvakou, Cihon and Demirer (2023) recruited 95 professional programmers through a freelancing platform and asked them to write an HTTP server in JavaScript as quickly as possible. An HTTP server is a small program that answers requests from web browsers; the task was self-contained, clearly specified and of a kind that appears often in public code. The programmers were randomly assigned to work with or without GitHub Copilot. Those with the assistant finished in an average of about 71 minutes, compared with about 161 minutes for the control group, which is 55.8 percent less time. The authors also reported signs that developers with less programming experience gained more, although that estimate was imprecise.
Experienced developers on their own projects took 19 percent longer
METR, a research organization that evaluates AI systems, ran a randomized trial with 16 experienced open-source developers (Becker, Rush, Barnes and Rein, 2025). The developers worked on 246 real tasks, such as bug fixes, new features and reorganizations of code, in large and mature software projects to which they had contributed for years; the projects averaged more than 22,000 stars on GitHub and more than one million lines of code. Each task was randomly assigned to allow or disallow AI tools, and when AI was allowed the developers mostly used an AI-enabled code editor with leading models of early 2025. Tasks completed with AI took 19 percent longer. Before the study, the developers had forecast that AI would cut their time by 24 percent, and after it they still believed that AI had made them about 20 percent faster.
METR stated the limits of its result plainly: the trial is a snapshot of early-2025 tools in one setting, it does not show that most developers or most programming work are slowed by AI, and AI may help more with unfamiliar code or less experienced developers. METR has continued to publish measurements as the tools have changed.
Figure 8.1 sets the results of the two studies side by side, using the figures their authors reported.
Reading the two studies together
Both studies were randomized, and their settings differed in three ways. The tasks differed: a new, self-contained program of a familiar kind in the first study, and changes to large, complex code with high standards for quality in the second. The people differed: freelance professionals meeting a task for the first time, and experts who already knew their code in detail. The most reasonable reading is that the gains from AI coding tools depend on the task and on the user's relationship to it. When a task is well defined and common, an assistant's first answer is usually close to right and the time saved is large. When an expert already knows what the code should do and must review, correct and adapt the assistant's suggestions to a complex project, the review can cost more time than the assistant saves.
The METR result also shows a gap between perception and measurement. Developers who were slower with AI felt faster, which suggests that a person's impression of an AI-assisted session is an unreliable guide to its real effect. For a data analyst, the corresponding risk concerns correctness more than speed: an analysis produced quickly with an assistant feels finished, and only checks of the kind taught in Section 3 can show whether it is right.
The next part considers what this evidence implies for a student who has never written R.
What this means for a beginner in R
A student who has never programmed differs from the participants in both studies. The potential gain is larger, because the alternative is often no analysis at all, and the ability to judge the code is smaller, because the student cannot yet tell good R from bad R at a glance. That combination explains the design of this lesson. Students do not need to write R from memory to use it well with an assistant, but they do need to read each line well enough to say what it does, run the code in small steps, read the output and apply checks that would reveal an error. Lesson 3 described AI capability as a jagged frontier, strong on some tasks and weak on others that look similar. For R, the frontier runs roughly as the cards below describe.
The accordion that follows takes up three questions about learning R with an assistant: whether the language still needs to be learned, which form of assistance to start with, and whether code written with an assistant counts as the student's own work.
A user who cannot read R cannot check it, and Section 3 shows that AI-written code can run without an error message and still give a wrong answer. The level of fluency needed changes, however. A beginner working with an assistant needs to read code, recognize common functions, understand data types and interpret output, while the exact spelling of function arguments can be left to the assistant. HSCI 410 Lesson 1 sets up R and RStudio, and working through an analysis with an assistant, asking for explanations at each step, is itself an effective way to learn the language.
A chat assistant, or the chat panel of an editor assistant, is the best starting point, because every exchange is visible and the student runs each piece of code personally. Code-executing data tools give answers without showing the code by default, and coding agents make many changes at once; both are easier to use well after the student can read R comfortably.
The analyst who runs and reports an analysis is accountable for it, whoever wrote the code. Whether AI help is permitted in a particular course is set by the instructor under SFU's guidelines, and permitted use must be disclosed, as Lesson 3 explained. In professional settings, the AI use log and comments in the script record which parts the assistant drafted and how they were checked.
Box 8.2 asks the reader to match four tasks to the form of coding assistance that suits each and to name one risk to watch for.
For each of the following four tasks, choose the form of coding assistance that suits it best (chat assistant, editor assistant, coding agent or code-executing data tool) and name one risk to watch for. (1) A student wants to understand what a ten-line R script from a classmate does. (2) An analyst wants the variable name region changed to health_region in fifteen scripts in a project folder. (3) A student wants a quick look at the distribution of a variable in a public spreadsheet of national immunization coverage. (4) An analyst is writing a ggplot2 figure in RStudio and wants suggestions as the code is typed.
(1) A chat assistant suits the explanation of a short script; the risk is an explanation that sounds right but misreads the code, so the student should run the script and confirm that the output matches the explanation. (2) A coding agent suits a repetitive change across many files; the risk is a change in the wrong place, so the analyst should review the list of edits before accepting them and rerun every script. (3) A code-executing data tool suits a first look at public, non-confidential data; the risk is a hidden computation, so the student should ask to see the code and check one number against the spreadsheet. (4) An editor assistant suits writing code in place; the risk is accepting a suggestion without reading it, so the analyst should read each suggested line before accepting it.
Jordan settles on the pilot's chat assistant for the analysis, because each step will then be visible, logged and run by Jordan personally. Section 2 sets out the workflow Jordan follows and the first four steps of the analysis.
Before that, the reflection and knowledge check that follow review the forms of coding assistance and the evidence on their effect on speed.
Reflection
A classmate who has a summer research position says: "A study showed that programmers with AI were 56 percent faster, so I will have an assistant write my whole analysis script in one go and skip learning R." Two studies are relevant. In Peng and colleagues (2023), 95 freelance professional programmers wrote an HTTP server in JavaScript, a small and clearly specified program; those randomly assigned to use GitHub Copilot finished 55.8 percent faster (about 71 minutes compared with about 161). In a randomized trial by METR (2025), 16 experienced open-source developers completed 246 real tasks in large projects they had worked on for years; tasks done with AI took 19 percent longer, although the developers believed afterward that AI had made them about 20 percent faster. Using these facts, explain to your classmate what the evidence shows and where the plan goes beyond it, and describe how you would recommend using an assistant for a first analysis in R.
The 56 percent figure is real, but it comes from one kind of task: a small, clearly specified program of a familiar type, written from scratch by professionals. The METR trial shows that the gain is not universal. Experienced developers working on complex code they knew well took 19 percent longer with AI and still felt faster. Read together, the studies suggest that the benefit depends on the task and on whether the user can judge the code, and that impressions of speed are unreliable.
For a first analysis in R, the potential gain is large, because an assistant can supply syntax the student does not know. The weak point is judgement: a student who cannot read R cannot tell whether a whole script generated in one reply is right, and code can run without errors and still give wrong answers. I would recommend describing the data to the assistant through its data dictionary, asking for one small step at a time, running each step and reading the output, and asking the assistant to explain every function used. After each step I would check row counts and missing values, and I would recompute key numbers by a second method. Working this way still saves time, and it builds the reading fluency in R that checking requires.
Minimum 20 characters required.
Question 1: In the controlled experiment by Peng and colleagues (2023), what task did the programmers complete, and what was the main result?
Question 2: In the METR (2025) trial with experienced open-source developers, how did the developers' beliefs compare with the measured effect of AI tools?
Question 3: Which form of coding assistance plans a task, edits files across a project, runs the code and reads the results, repeating these steps until it judges the task complete?
Question 4: A student asks a chat assistant, which has never seen the data, for code to compute mean age, and receives mean(df$age). The student's data frame is called cscs and its age column is DEMO_age. What best explains the output, and what should the student do?
Writing and Understanding R with AI
Learning Objectives for this section
- Describe a dataset to an AI assistant through its data dictionary instead of uploading the records.
- Request R code in small steps, and run and read the output of each step before moving to the next.
- Read basic R syntax (assignment, the pipe, functions and their arguments, and comparisons) well enough to say what a line of code does.
- Use an assistant to explain unfamiliar functions and error messages.
- Save an analysis as a commented script or Quarto document and record the assistant's role in an AI use log.
An assistant can produce a complete analysis script in one reply, and for a beginner that is the least useful thing it can do. A long script that has never been run hides its errors among dozens of lines, and a student who cannot yet read R has no way to find them. The workflow in this section slows the process down to a pace at which each piece of code is understood and checked before the next one is written. It assumes no prior experience with R. Students who have not yet installed R and RStudio can follow the setup in HSCI 410 Lesson 1, and HSCI 207 Lesson 11 explains data dictionaries and missing-value codes using the same survey.
A workflow for writing R with an assistant
The workflow in this lesson turns a request for code into a sequence of small steps, each of which is run, read and checked before the next is written. Figure 8.2 shows the workflow as a whole, in which most steps repeat for every small piece of the analysis and the script is saved and logged once the last check has passed. The subsections that follow describe its parts: describing the data, asking for code in small steps, running and reading each step, asking for explanations and saving the work.
Describe the data through its data dictionary
The assistant writes code for data it cannot see, so the quality of its code depends on the description it receives. The most efficient description is the relevant part of the data dictionary: each variable's exact name, the question it records, its type (number, text or category), its permitted values and its missing-value codes. A missing-value code is a value that stands for a missing answer, such as 999, -88 or, in the Canadian Social Connection Survey, the text "Presented but no response". Describing the data in this way gives the assistant what it needs without sending a single record, which is also the habit that Section 4 requires for confidential data. A useful prompt adds who the user is, the question the analysis must answer and an instruction to ask for anything the dictionary does not say.
Ask for code in small steps
Each request should cover one step that produces output the user can inspect: load the data and count the rows, keep the needed variables, recode one kind of missing value, create one new variable. Small steps keep each block of code short enough to read, they make an error easy to locate, and they create a natural point at which to check the result. It also helps to ask the assistant what output to expect, because a prediction made before the code is run gives the user something to compare against.
Run and read each step
The user runs every block in an R session (in RStudio, Positron or the R console) and reads all of the output. A warning means that R completed the step but noticed something worth attention, such as values it could not convert. An error means that R stopped without completing the step, which is the safer kind of failure, because a step that stops cannot silently produce a wrong number.
Ask for explanations
After each step, the user can ask what a function does, what each argument means and what an error message means. Over a few analyses, these questions build the reading fluency in R that checking requires.
Save the work
Code that has been run and checked goes into a single script, with a comment on each step, so that the whole analysis can be rerun from the top. The script, together with the AI use log introduced in Lesson 3, records what the assistant drafted and how each step was checked.
The workflow asks a student to read each block of code and its output. The next part sets out the minimum R notation needed to do so.
Reading R: a minimum vocabulary
A beginner can read most of the code in this lesson with the eight pieces of notation in Table 8.2. In nrow(cscs), R applies the function nrow(), which counts rows, to the object called cscs. A function is a named operation that takes inputs, called arguments, inside its parentheses and returns a result.
Table 8.2. Eight pieces of R notation, with their meaning in plain language and an example of each.
| Notation | Meaning in plain language | Example |
|---|---|---|
<- | Store the result on the right under the name on the left. | cscs <- data |
|> | The pipe: pass the result on the left into the function on the right. Read it as "and then". | data |> filter(...) |
f(x, n = 3) | Call the function f with the input x and the named argument n set to 3. | head(cscs, n = 3) |
== | Test whether two values are equal (two equals signs; one equals sign names an argument). | year == 2021 |
$ | Take one column from a data frame. | lonely$gender |
c() | Combine values into a list of values, called a vector. | c(16, 30, 45) |
NA | A missing value. | is.na(x) |
# | A comment: R ignores the rest of the line. | # keep 2021 |
With this vocabulary, the code in the demonstration that follows can be read line by line. Worked Example 8.1 begins the demonstration that runs through the rest of the lesson: Jordan describes the survey to the assistant through its data dictionary and asks for a plan and the code for the first step only.
Jordan opens the pilot's chat assistant, describes the survey through its data dictionary and asks for a plan and the code for the first step only. The assistant's replies in this lesson are representative examples prepared for teaching, and they are not the verbatim responses of any named product. The R code attributed to the assistant was run on the survey file, and every R output on this page was pasted unchanged from that run. The demonstration continues through Worked code 8.1 to Worked code 8.4 and into Section 3.
Prompt
I am a co-op student with no R experience. I am analyzing the 2021 wave of the Canadian Social Connection Survey, a public, de-identified file that loads into R as a data frame called data (13,219 rows from four survey waves). My question is: what percentage of respondents in each of four age groups (16 to 29, 30 to 44, 45 to 64, and 65 and older) scored 6 or higher on the three-item UCLA Loneliness Scale? I also need code that scores the three items myself, because a program I work with records the same three items, and I can use the survey's own score as a check. The data dictionary entries are below. Please (1) propose a plan in small steps, (2) give me the R code for step 1 only, using dplyr, with a comment on each line, and (3) tell me what output to expect so that I can check it. Do not assume anything about the data that the dictionary does not say; ask me if you need more information.
SURVEY_collection_year: survey wave (2021, 2022, 2023 or 2024) DEMO_age: integer, "How old are you (in years)?" (16 to 100 in 2021) DEMO_gender: factor; Man, Woman, Non-binary, Presented but no response GEO_province: factor; 13 provinces and territories, Presented but no response LONELY_ucla_loneliness_scale_companionship: ordered factor, "How often do you feel that you lack companionship?" Hardly Ever, Some of the time, Often, Presented but no response LONELY_ucla_loneliness_scale_left_out: ordered factor, "How often do you feel left out?" (same options) LONELY_ucla_loneliness_scale_isolated: ordered factor, "How often do you feel isolated from others?" (same options) LONELY_ucla_loneliness_scale_score: integer, 3 to 9 (items scored 1 to 3, added) Codes: "Presented but no response" = question shown and left blank; NA = a question never answered.
Output (abridged)
Here is a plan in six steps. (1) Load the file, keep the 2021 wave and count the rows. (2) Keep the seven variables you listed and give them short names. (3) Convert "Presented but no response" to NA in gender and province, and count the missing values in every column. (4) Score each UCLA item (Hardly Ever = 1, Some of the time = 2, Often = 3), add the three scores, and compare your total with LONELY_ucla_loneliness_scale_score. (5) Create the four age groups. (6) Make a table of the number of respondents, the mean score and the percentage scoring 6 or higher in each age group, and draw two plots.
The code for step 1 is below [the code shown in the first R box]. nrow(cscs) should print one number, the count of 2021 respondents, which you can compare with the count your data dictionary gives for the 2021 wave. If it prints 0, the year may be stored as text, and we will adjust the filter.
One question for you: should respondents whose gender is "Presented but no response" stay in the age-group table? Gender does not affect the age groups, so I suggest keeping them.
Commentary. The plan is sound. It works in small steps, it proposes the comparison with the survey's own score that Jordan asked for, it states an expected result for step 1, and it asks a question in place of making an assumption, which Jordan answers by keeping those respondents. The plan also leaves two things out. It says nothing about the survey's design, so Jordan adds a note that the survey recruited respondents online and is cross-sectional, which limits generalization to the Canadian population (the limitation that HSCI 410 Lesson 6 states for this survey). Step 4 also does not say what happens to "Presented but no response" on the loneliness items; that gap becomes an error in Section 3. The first check is applied as soon as step 1 runs: nrow(cscs) printed 4045, and the CSCS data dictionary lists 4,045 respondents in the 2021 wave, so the filter kept the right rows. The checks for the later steps appear with each R box.
The R code, output and checks for the first four steps follow. Worked code 8.1 loads the survey and keeps the 2021 wave, Worked code 8.2 shows the error produced by code written before the assistant saw the dictionary, Worked code 8.3 selects and renames the seven variables, and Worked code 8.4 counts the missing values and recodes the missing-value code.
The first three lines load the packages. Loading dplyr prints a message that some objects "are masked from 'package:stats'"; this is normal and means that dplyr's filter() replaces a function of the same name. The next two lines download the survey file and load it as a data frame called data, which requires an internet connection. The pipe then passes data to filter(), which keeps only the rows in which the survey year equals 2021, and the arrow stores the result as cscs.
library(dplyr) # verbs for data frames: filter, select, mutate, summarise
library(readr) # write_csv() and read_csv()
library(ggplot2) # plots
github_url <- "https://raw.githubusercontent.com/jorgeandr3s/heal/main/cscs/public_data/CSCS2025_full_cleaned_deidentified_data_and_metadata.RData"
load(url(github_url)) # creates a data frame called data
cscs <- data |>
filter(SURVEY_collection_year == 2021) # keep the 2021 wave only
nrow(cscs) # rows: one per respondent
ncol(cscs) # columns: one per variable
[1] 4045
[1] 3247
The [1] at the start of each line is R's numbering of output values and can be ignored. The 2021 wave has 4,045 respondents, matching the data dictionary, and the file has 3,247 columns because it holds every variable from all four waves. Selecting the few variables the analysis needs is the next step.
Before writing the prompt in Worked Example 8.1, Jordan had tried a shorter request with no dictionary: "Give me R code to keep age, gender, province and the UCLA loneliness score from my data frame." The assistant proposed the code below. It has no way of knowing the real column names, so it invented short ones. The try() wrapper is there only so that the downloadable script keeps running after the error. At the console, the inner lines alone stop with the same error, laid out slightly differently and headed Error in `select()`.
try(
lonely <- cscs |>
select(age, gender, province, ucla_score)
)
Error in select(cscs, age, gender, province, ucla_score) :
Can't subset columns that don't exist.
x Column `age` doesn't exist.
Prompt
I ran your code and R printed this error: "Can't subset columns that don't exist. x Column `age` doesn't exist." What does this mean, in plain language, and how do I fix it? [Jordan pastes the dictionary entries for the seven variables.]
Output (abridged)
The message means that select() looked for a column called age in cscs and did not find one. My earlier code guessed short names because I had not seen your data. R reports only the first missing column, but gender, province and ucla_score do not exist either. In select() you can keep a column and rename it in one move by writing the new name, an equals sign and the dictionary name, for example age = DEMO_age.
The explanation is correct and matches the output, in which R named only age. An error of this kind is a safe failure, because R stopped before producing anything; guesses about codes and types, by contrast, produce no error at all.
With the dictionary in the conversation, the assistant's code names each column correctly. Inside select(), the short name on the left of each equals sign is the new name, and the dictionary name on the right is the column being kept. Keeping the survey's own score under the name ucla_survey preserves it for the comparison planned in step 4. The function glimpse() prints one line per column with its name, its type and its first few values.
lonely <- cscs |>
select(age = DEMO_age,
gender = DEMO_gender,
province = GEO_province,
companionship = LONELY_ucla_loneliness_scale_companionship,
left_out = LONELY_ucla_loneliness_scale_left_out,
isolated = LONELY_ucla_loneliness_scale_isolated,
ucla_survey = LONELY_ucla_loneliness_scale_score)
glimpse(lonely) # one line per column: name, type, first values
Rows: 4,045
Columns: 7
$ age <int> 58, 35, 39, 55, 70, 60, 77, 37, 50, 24, 27, 29~
$ gender <fct> Woman, Non-binary, Man, Woman, Woman, Woman, M~
$ province <fct> Alberta, British Columbia, British Columbia, O~
$ companionship <ord> Often, Some of the time, Often, Some of the ti~
$ left_out <ord> Often, Often, Often, Some of the time, Often, ~
$ isolated <ord> Often, Some of the time, Often, Hardly Ever, O~
$ ucla_survey <int> 9, 7, 9, 5, 9, 6, 3, 5, 8, 8, 5, 6, 7, 3, 4, 6~
The output confirms 4,045 rows and 7 columns; the ~ marks a line cut short to fit the screen. The types in angle brackets matter for what follows. <int> means whole numbers. <fct> means a factor, which is R's type for a categorical variable: the values are stored as categories, called levels, in a fixed order. <ord> means an ordered factor, a factor whose levels have a meaningful order, as the three loneliness answers do. Section 3 shows why the stored order of the levels can matter a great deal.
Before any recoding, Jordan asks for code that counts the missing values in every column and lists every answer to the gender question, including NA. The function is.na() marks each missing value as TRUE, and colSums() adds up the TRUE values in each column. The argument useNA = "ifany" tells table() to show a count of NA as well, which it leaves out by default.
colSums(is.na(lonely)) # NA count in each column
table(lonely$gender, useNA = "ifany") # every answer, NA included
age gender province companionship left_out
0 3 0 421 421
isolated ucla_survey
421 462
Man Woman
1883 2049
Non-binary Presented but no response
106 4
<NA>
3
Two things stand out. Each loneliness item has 421 missing values, while the survey's score has 462, so 41 respondents with all three items present have no score; Section 3 returns to them. Gender has two kinds of missing answer: 3 NA values and 4 respondents recorded as "Presented but no response". The assistant's next block converts that code to NA in gender and province. The function mutate() changes or creates columns, as.character() turns the factor into plain text, and na_if() replaces one stated value with NA.
lonely <- lonely |>
mutate(gender = na_if(as.character(gender),
"Presented but no response"),
province = na_if(as.character(province),
"Presented but no response"))
table(lonely$gender, useNA = "ifany") # the 4 codes are now NA
sum(is.na(lonely$province)) # 50 codes became NA
summary(lonely$age) # plausible range for adults?
Man Non-binary Woman <NA>
1883 106 2049 7
[1] 50
Min. 1st Qu. Median Mean 3rd Qu. Max.
16.0 28.0 34.0 39.8 52.0 100.0
Each result can be checked against the earlier output. Gender now has 7 missing values, the 3 original NA values plus the 4 converted codes, and the other categories keep their counts of 1,883, 106 and 2,049. Province has 50 missing values, all of them converted codes, since province had no NA values before. Ages run from 16 to 100 with a median of 34, which is plausible for an adult survey and matches the dictionary's stated range. Jordan logs the step as complete.
By the end of these four steps, the survey file has been loaded, reduced to seven variables and checked for missing values. The next part turns to prompts that ask the assistant to explain the code at each step.
Asking for explanations
Every block in Worked code 8.1 to Worked code 8.4 contains functions that a beginner has not met before. Each of the prompts below asks the assistant to explain the code the student is actually running, which keeps the explanations tied to real output.
"Explain this code line by line for someone who has never used R. For each function, say what it does and what each argument means." This is the most useful general prompt. Paste the exact code that was run, so that the explanation describes that code and not a similar version.
"What would happen if I removed useNA = "ifany" from this line?" Questions of this kind teach what each argument contributes. The student can test the answer immediately by running the changed line, which also checks the assistant's explanation.
"What assumptions does this code make about my data? List anything that would make it give a wrong answer without an error." This prompt draws out hidden assumptions about codes, types and missing values, which are the main source of silent errors. It works best when the dictionary entries are in the conversation.
"I ran this code and R printed this message: [paste the full message]. Explain what it means and suggest a fix." Include the code, the complete message and the relevant dictionary entries. Then ask why the fix works, so that the same error is recognizable next time.
Explanations of this kind build the reading fluency that checking requires. Once a step has been explained and checked, its code is saved, which the next part describes.
Saving the work
Once a step has run and passed its checks, its code goes into the script in order, with a comment that says what the step does. The downloadable script for this lesson follows that pattern, and it labels the blocks that contain the assistant's errors. A script should run from a fresh R session without any manual steps, so the final test of a saved script is to restart R and run it from the first line to the last.
Analysts who want text, code and output in one report can use a Quarto document, a file with the extension .qmd that mixes paragraphs of writing with chunks of R code. Quarto, an open-source publishing system developed by Posit, runs the code and inserts the output into an HTML, Word or PDF report, so the numbers in the report always come from the code.
The AI use log introduced in Lesson 3 records the assistant's part in the analysis. A short entry for each working session is enough, as in Jordan's first entry in Table 8.3.
Table 8.3. Jordan's AI use log entry for the first analysis session.
| Log field | Jordan's entry |
|---|---|
| Date and tool | First analysis session; the pilot's enterprise chat assistant. |
| Data shared | Data dictionary entries for seven public CSCS variables. No records were shared. |
| What the assistant produced | A six-step plan; code for steps 1 to 4; an explanation of one error message. |
| Checks applied | Row count 4,045 matched the dictionary; missing values counted before and after recoding (gender 3 to 7, province 0 to 50); age range 16 to 100. |
| Changes made | Replaced guessed column names with dictionary names; added a note on survey design. |
Box 8.3 asks the reader to explain a single line of R in plain words, using the notation introduced in this section.
Without running it, explain in plain words what the following line does, using the vocabulary in Table 8.2, and say what the result would be if every value of age in the British Columbia rows were missing: mean(lonely$age[lonely$province == "British Columbia"], na.rm = TRUE). (The square brackets after a column keep only the values for which the condition inside them is TRUE, and na.rm = TRUE tells mean() to remove missing values before averaging.)
The line takes the age column of lonely, keeps only the values in rows where province equals "British Columbia", removes any missing values and returns their mean, which is the average age of respondents from British Columbia. If every one of those ages were missing, there would be nothing left to average after the missing values were removed, and R would return NaN ("not a number"). A careful analyst would also notice that the 50 respondents whose province is now NA are excluded by the condition, which is correct for this question but worth stating.
The first four steps produced a file of 4,045 respondents with the right variables and with the gender and province codes converted. The assistant's remaining code (scoring the items, building the age groups, the table and the plots) runs without a single error message. Section 3 shows that two of those steps are nonetheless wrong, and how the checks find them.
Before that, the reflection and knowledge check that follow review the workflow, the data dictionary and the first four steps of the analysis.
Reflection
You are about to analyze a public survey file with an AI assistant. The file has been loaded into R as a data frame called sleep with 2,000 rows. Its data dictionary lists three variables: resp_age (integer, 18 to 99), sleep_hours ("On a typical night, how many hours do you sleep?", numeric, 0 to 24, with the value 99 meaning "Don't know") and region (factor with the levels North, South, Coast and Refused). You want the mean hours of sleep in each region. Write the prompt you would send to the assistant to obtain the code for the first step, and explain what each part of the prompt is for. Then describe two things you would check in the output of that first step before asking for the next one.
My prompt: "I am new to R. I have a public survey data frame called sleep with 2,000 rows. Data dictionary: resp_age, integer, 18 to 99; sleep_hours, numeric, 0 to 24, where 99 means Don't know; region, factor: North, South, Coast, Refused. My goal is the mean hours of sleep in each region. Please propose a plan in small steps, then give me only the code for step 1, which should count the rows and show every value of region and a summary of sleep_hours, with a comment on each line. Tell me what output to expect, and ask me if anything is unclear."
The first sentence sets the level of explanation. The dictionary gives the exact names, types and codes, so the assistant does not guess, and no records are shared. Stating the goal lets the assistant plan, while asking for step 1 only keeps the code short enough to read. Asking for expected output gives me something to compare with, and the invitation to ask questions reduces silent assumptions.
In the output I would first check that nrow() prints 2000 and that the table of region, with useNA = "ifany", shows how many answered Refused, which must be treated as missing. Second, I would check that the summary of sleep_hours has a maximum of 99, which confirms that the Don't know code is present and must be converted to NA before any mean is calculated.
Minimum 20 characters required.
Question 1: Why does the workflow in this lesson describe the data to the assistant through its data dictionary instead of uploading the records?
Question 2: What does the line cscs <- data |> filter(SURVEY_collection_year == 2021) do?
Question 3: After running select(cscs, age, gender, province, ucla_score), R prints: "Can't subset columns that don't exist. x Column `age` doesn't exist." What is the best next step?
age = DEMO_age inside select().Question 4: Before recoding, table(lonely$gender, useNA = "ifany") showed 4 answers of "Presented but no response" and 3 NA values. After the code converts the missing-value code to NA, how many NA values should gender have?
Checking an Analysis
Learning Objectives for this section
- Explain why code that runs without an error message can still give a wrong answer.
- Apply four checks to an AI-written analysis: row counts and missing values, recomputation by a second method, plots of the data, and a test on a small dataset with a known answer.
- Diagnose and correct two common errors in AI-written R code: a missing-value code counted as a number and an age group with the wrong boundaries.
- Explain the garden of forking paths and how AI makes it easy to run many analyses and report the most favourable one.
- Describe what a pre-specified analysis plan contains, and explain why the choice and interpretation of an analysis remain the analyst's responsibility.
When R stops with an error message, the problem is visible and an assistant can usually explain it. The harder case is code that runs to the end and prints a tidy result that is wrong. Such silent errors arise most often where the code meets the data: a missing-value code that the code treats as a real answer, a boundary that puts people in the wrong group, or a filter that removes rows without saying so. An assistant that has been told little about the data makes these errors easily, and a beginner cannot spot them by reading the code. They are found by checking the results.
Four checks for an AI-written analysis
The four checks in Table 8.4 catch most silent errors in descriptive analyses. None of them requires advanced R, and an assistant can write the code for each one. The effort should rise with the stakes, as Lesson 3 recommended for verification in general: a figure for a planning meeting deserves every check, and an exploratory look at a variable may need only the first.
Table 8.4. Four checks for an AI-written analysis, with what each one catches and how to carry it out in R.
| Check | What it catches | How to do it in R |
|---|---|---|
| Confirm row counts and missing values | Rows lost or duplicated by a filter; values turned into NA; missing-value codes left in as answers | nrow(), table(x, useNA = "ifany"), colSums(is.na()), summary() |
| Recompute by a second method | Errors of logic that produce plausible numbers | A different function, or a variable that someone else computed independently |
| Plot the data | Impossible values, outliers and unexpected shapes | A bar chart or histogram of every new variable |
| Test on data with a known answer | Errors in a scoring rule or function | A few invented rows whose answers are worked out by hand |
The two parts that follow apply these checks to the next two steps of Jordan's analysis, the scoring of the loneliness items and the construction of the age groups.
Check in action: scoring the loneliness items
The next step in Jordan's plan scores the three UCLA items and adds them. The demonstration continues with the prompt, the assistant's code as proposed, and the real output.
Box 8.4 gives Jordan's prompt for this step and Box 8.5 the assistant's reply. Worked code 8.5 shows the code as proposed, with a range check and a comparison against the survey's own score applied to its output.
Box 8.4: Prompt
Step 4: write code that scores each of the three UCLA items (Hardly Ever = 1, Some of the time = 2, Often = 3) and adds the three scores into a new column called ucla_score.
Box 8.5: Output (abridged)
The three items are ordered factors whose levels are already in the order Hardly Ever, Some of the time, Often, so as.numeric() converts them to 1, 2 and 3 directly. [The code shown in Worked code 8.5.]
The code runs without a warning. Jordan's first check is to count every value of the new score, including NA.
lonely <- lonely |>
mutate(ucla_score = as.numeric(companionship) +
as.numeric(left_out) +
as.numeric(isolated))
table(lonely$ucla_score, useNA = "ifany") # every value, NA included
3 4 5 6 7 8 9 10 12 <NA>
494 490 806 888 403 209 315 4 15 421
The scale runs from 3 to 9, yet 4 respondents have a score of 10 and 15 have a score of 12. A range check of this kind has caught 19 impossible values. The second check compares Jordan's score with the score the survey team computed, by counting the rows in which each one is missing.
# Second method: compare with the score the survey team computed
lonely |>
count(ours_missing = is.na(ucla_score),
survey_missing = is.na(ucla_survey))
# Look at the rows where the two disagree
lonely |>
filter(!is.na(ucla_score), is.na(ucla_survey)) |>
select(companionship, left_out, isolated, ucla_score) |>
head(3)
levels(lonely$companionship) # the order of the levels decides the numbers
ours_missing survey_missing n
1 FALSE FALSE 3583
2 FALSE TRUE 41
3 TRUE TRUE 421
companionship left_out
1 Presented but no response Presented but no response
2 Hardly Ever Some of the time
3 Presented but no response Presented but no response
isolated ucla_score
1 Presented but no response 12
2 Presented but no response 7
3 Presented but no response 12
[1] "Hardly Ever" "Some of the time"
[3] "Often" "Presented but no response"
Forty-one respondents have a score from Jordan's code and none from the survey. They are the same 41 people noticed in Section 2, who had all three items present but no survey score. The rows printed show why: each has at least one answer of "Presented but no response", and levels() shows that this code is the fourth level of the factor. For a factor, as.numeric() returns the position of each answer's level, so the missing-value code was counted as an answer worth 4. The assistant's statement about the order of the first three levels was true, and the code still failed because it ignored the fourth.
The consequences reach further than the 19 impossible values. Since the smallest possible wrong score is 1 + 1 + 4 = 6, every one of the 41 respondents would have been counted as screening positive at the Connector threshold. Comparing the table in Worked code 8.5 with the corrected table in Worked code 8.6 shows that 22 of the 41 wrong scores fell inside the valid range (10 extra respondents at 6, 6 at 7, 3 at 8 and 3 at 9). A range check alone would have missed more than half of the errors, and only the comparison with an independent second method found them all. HSCI 207 Lesson 11 makes the same point about numeric codes such as 999: a code treated as a number distorts every summary that includes it.
Worked code 8.6 gives the corrected scoring code, which states the scoring rule in full and confirms the result with a second method and a known-answer test.
The corrected code, which Jordan asked the assistant to write with the dictionary's codes spelled out, states the scoring rule in full. The first block creates a small function of Jordan's own called score_item(), so that one rule is applied to all three items. Inside it, case_when() checks each condition in turn and returns the value after the tilde (~) for the first condition that is TRUE; the last line, TRUE ~ NA_real_, catches every other answer, including the missing-value code, and returns a missing number.
score_item <- function(x) {
x <- as.character(x)
case_when(x == "Hardly Ever" ~ 1,
x == "Some of the time" ~ 2,
x == "Often" ~ 3,
TRUE ~ NA_real_)
}
lonely <- lonely |>
mutate(ucla_score = score_item(companionship) +
score_item(left_out) +
score_item(isolated))
table(lonely$ucla_score, useNA = "ifany")
all.equal(lonely$ucla_score, lonely$ucla_survey) # second method agrees?
3 4 5 6 7 8 9 <NA>
494 490 806 878 397 206 312 462
[1] TRUE
All scores now lie between 3 and 9, there are 462 missing scores, and all.equal() confirms that Jordan's score matches the survey's score for every one of the 4,045 respondents, with the missing values in the same rows. The last check runs the function on four invented respondents whose scores can be worked out by hand: 1 + 1 + 1 = 3, 3 + 3 + 3 = 9, 2 + 2 + 3 = 7, and a missing score for the person with a missing-value code.
test <- data.frame(
companionship = c("Hardly Ever", "Often", "Some of the time", "Often"),
left_out = c("Hardly Ever", "Often", "Some of the time",
"Presented but no response"),
isolated = c("Hardly Ever", "Often", "Often", "Often"))
test |>
mutate(score = score_item(companionship) + score_item(left_out) +
score_item(isolated))
# Expected by hand: 3, 9, 7 and NA
companionship left_out isolated score
1 Hardly Ever Hardly Ever Hardly Ever 3
2 Often Often Often 9
3 Some of the time Some of the time Often 7
4 Often Presented but no response Often NA
The function returns 3, 9, 7 and NA, as expected. Because the Connector program records the same three items, this tested function can be reused on the program's own screening data, where no survey score will exist to compare against. A known-answer test of this kind is the check that remains available when there is no second method.
With the scores corrected and confirmed, the analysis moves to step 5 of Jordan's plan, the construction of the age groups, where a second silent error appears.
Check in action: building the age groups
Age groups are built by cutting a continuous variable at chosen boundaries, and errors in grouping arise at those boundaries. Box 8.6 gives Jordan's prompt for step 5 and Box 8.7 the assistant's reply. Worked code 8.7 shows the code as proposed, the checks that expose its two errors and the correction.
Box 8.6: Prompt
Step 5: create a column called age_group with four groups: 16 to 29, 30 to 44, 45 to 64, and 65 and older.
Box 8.7: Output (abridged)
The function cut() divides a number into intervals at the break points you give it and labels each interval. [The code shown in the R box below.]
The code runs without a message. Jordan counts the groups, including NA, and then asks for the youngest and oldest age in each group, which is a quick way to see where each boundary actually falls. The function group_by() splits the data by age group, and summarise() computes one row of results per group.
age_labels <- c("16 to 29", "30 to 44", "45 to 64", "65 and older")
lonely <- lonely |>
mutate(age_group = cut(age, breaks = c(16, 30, 45, 65, Inf),
labels = age_labels))
table(lonely$age_group, useNA = "ifany")
lonely |>
group_by(age_group) |>
summarise(n = n(), youngest = min(age), oldest = max(age))
16 to 29 30 to 44 45 to 64 65 and older <NA>
1619 1203 774 439 10
# A tibble: 5 x 4
age_group n youngest oldest
<fct> <int> <int> <int>
1 16 to 29 1619 17 30
2 30 to 44 1203 31 45
3 45 to 64 774 46 65
4 65 and older 439 66 100
5 <NA> 10 16 16
The missing-value count shows the first problem: 10 respondents have no age group, although no ages are missing, and the summary shows that all 10 are aged 16. The youngest and oldest columns show the second problem: the group labelled "16 to 29" contains 30-year-olds, "45 to 64" contains 65-year-olds, and "65 and older" starts at 66. By default, cut() builds intervals that include their upper boundary and exclude their lower one, so the first interval runs from just above 16 up to and including 30, and the labels describe groups that the intervals do not contain.
# CORRECTED: right = FALSE makes each group include its lower boundary
lonely <- lonely |>
mutate(age_group = cut(age, breaks = c(16, 30, 45, 65, Inf),
labels = age_labels, right = FALSE))
lonely |>
group_by(age_group) |>
summarise(n = n(), youngest = min(age), oldest = max(age))
# A tibble: 4 x 4
age_group n youngest oldest
<fct> <int> <int> <int>
1 16 to 29 1379 16 29
2 30 to 44 1395 30 44
3 45 to 64 785 45 64
4 65 and older 486 65 100
With right = FALSE, each group includes its lower boundary, every respondent has a group, and the youngest and oldest ages match the labels. The four groups add up to 4,045. The error had removed 47 people aged exactly 65 from the "65 and older" group (486 compared with 439), about one in ten of the group that matters most for the Connector program.
With the scores and the age groups corrected, the remaining steps save the cleaned file, compute the descriptive table and draw two plots, which the next part presents.
The cleaned file, the descriptive table and two plots
The last step of Jordan's plan produces the results that answer the team lead's question. Worked code 8.8 saves the cleaned file and reads it back, and Worked code 8.9 computes the descriptive table and confirms its last column by a second method.
The cleaned file keeps the variables the analysis needs and adds screen_positive, which is TRUE for a score of 6 or higher (the Connector threshold). After saving the file with write_csv(), Jordan reads it back and counts its rows, which confirms that nothing was lost on the way to the file.
clean <- lonely |>
mutate(screen_positive = ucla_score >= 6) |> # Connector threshold
select(age, age_group, gender, province, ucla_score, screen_positive)
write_csv(clean, "cscs2021_loneliness_clean.csv")
nrow(read_csv("cscs2021_loneliness_clean.csv", show_col_types = FALSE))
[1] 4045
The table removes respondents without a score, groups the rest by age and computes four summaries: the number of respondents, the mean and standard deviation of the score, and the percentage scoring 6 or higher. The mean of a TRUE or FALSE column is the proportion of TRUE values, so 100 * mean(screen_positive) gives a percentage. The survey also contains its own yes or no version of the same cut-off, LONELY_ucla_loneliness_scale_score_y_n, which provides an independent second method.
table1 <- clean |>
filter(!is.na(ucla_score)) |>
group_by(age_group) |>
summarise(n = n(),
mean_score = round(mean(ucla_score), 2),
sd_score = round(sd(ucla_score), 2),
pct_6_plus = round(100 * mean(screen_positive), 1))
table1
sum(table1$n) # respondents with a score
# Second method: the survey team's own yes/no version of the same cut-off
check <- table(clean$age_group, cscs$LONELY_ucla_loneliness_scale_score_y_n)
round(100 * prop.table(check, margin = 1), 1)
# A tibble: 4 x 5
age_group n mean_score sd_score pct_6_plus
<fct> <int> <dbl> <dbl> <dbl>
1 16 to 29 1251 5.49 1.46 49.6
2 30 to 44 1222 5.4 1.59 45.5
3 45 to 64 677 5.99 2.03 57.9
4 65 and older 433 5.65 2.09 51.7
[1] 3583
No (3-5) Yes (6-9)
16 to 29 50.4 49.6
30 to 44 54.5 45.5
45 to 64 42.1 57.9
65 and older 48.3 51.7
The filter removed the 462 respondents without a score, and the four groups contain the remaining 3,583. The second method gives the same four percentages (49.6, 45.5, 57.9 and 51.7), so the percentage column is confirmed. Among respondents aged 65 and older, 51.7 percent scored 6 or higher, compared with 57.9 percent at ages 45 to 64 and 45.5 percent at ages 30 to 44. The mean scores lie close together, from 5.4 to 5.99, while the standard deviations rise with age, from 1.46 in the youngest group to 2.09 in the oldest.
Plotting is the third check, and here it also adds to the description. The script draws both plots with ggplot2 and saves them as image files; Figure 8.3 and Figure 8.4 redraw them from the numbers that R printed. The R code for the first plot also prints the percentages it shows.
Worked code 8.10 gives the ggplot2 code for both plots.
The first block counts respondents at each score in two groups (under 65, and 65 and older) and converts the counts to percentages of each group. In ggplot2, aes() maps columns to the parts of the plot (score on the horizontal axis, percentage on the vertical axis, colour by group), and geom_col() draws bars of those heights. The second plot draws the last column of the table.
dist <- clean |>
filter(!is.na(ucla_score)) |>
mutate(group = if_else(age >= 65, "65 and older", "Under 65")) |>
count(group, ucla_score) |>
group_by(group) |>
mutate(pct = round(100 * n / sum(n), 1)) |>
ungroup()
print(dist, n = 14)
p1 <- ggplot(dist, aes(x = factor(ucla_score), y = pct, fill = group)) +
geom_col(position = "dodge") +
labs(x = "UCLA Loneliness Scale score (3 to 9)",
y = "Percent of age group", fill = NULL,
title = "Loneliness scores, CSCS 2021") +
theme_minimal()
ggsave("ucla_distribution_by_age.png", p1, width = 7, height = 4, dpi = 150)
p2 <- ggplot(table1, aes(x = age_group, y = pct_6_plus)) +
geom_col(fill = "#0B7B6B") +
geom_text(aes(label = paste0("n = ", n)), vjust = -0.4) +
labs(x = "Age group", y = "Percent scoring 6 or higher",
title = "Screen-positive for loneliness, CSCS 2021") +
theme_minimal()
ggsave("screen_positive_by_age_group.png", p2, width = 7, height = 4, dpi = 150)
# A tibble: 14 x 4
group ucla_score n pct
<chr> <dbl> <int> <dbl>
1 65 and older 3 100 23.1
2 65 and older 4 51 11.8
3 65 and older 5 58 13.4
4 65 and older 6 81 18.7
5 65 and older 7 40 9.2
6 65 and older 8 39 9
7 65 and older 9 64 14.8
8 Under 65 3 394 12.5
9 Under 65 4 439 13.9
10 Under 65 5 748 23.7
11 Under 65 6 797 25.3
12 Under 65 7 357 11.3
13 Under 65 8 167 5.3
14 Under 65 9 248 7.9
Figure 8.3 shows something the table's means conceal. Respondents aged 65 and older are spread toward both ends of the scale: 23.1 percent have the lowest possible score of 3 and 14.8 percent have the highest score of 9, compared with 12.5 percent and 7.9 percent of younger respondents. Younger respondents cluster in the middle, at scores of 5 and 6. This pattern is consistent with the larger standard deviation in the oldest group, and it means that "about half of older respondents screen positive" describes two quite different subgroups, one with little loneliness and one with a great deal. The plot also serves as a check: drawn from the uncorrected score, the same chart would have shown bars at 10 and 12 on a scale that ends at 9.
Jordan's summary for the team lead reports these figures as descriptive context and states their limits. The survey recruited respondents online and is cross-sectional, so the percentages describe the people who took part in 2021 and are not estimates for the Canadian population or for Cedar Valley, and no statistical test was run, so the differences between age groups are described without any claim that they would persist in another sample.
These results came from code that ran and checks that passed. Whether a descriptive table was the right analysis for the question is a separate decision, which the next part considers.
The choice of analysis belongs to the analyst
The assistant would have written equally fluent code for a regression model or a test across all thirteen provinces and territories. It cannot judge whether an analysis answers the question or whether its assumptions hold, and it tends to accept the framing it is given. Deciding that a descriptive table with a stated threshold answered the team lead's question was Jordan's decision, made with the epidemiologist. The methods themselves are taught in HSCI 410: Lesson 2 covers data cleaning and descriptive analyses, Lesson 3 covers regression, and Lesson 6 covers visualization with ggplot2.
Choosing one analysis also raises the question of what happens when an assistant makes many analyses easy to run, which the next part takes up.
Many analyses and the garden of forking paths
AI makes another analysis almost free. An analyst can ask for loneliness by province, by gender, by income, with a threshold of 6 or 7, with or without the youngest respondents, and receive working code for each in minutes. Reporting the comparison with the most striking difference invites a false positive, because among many comparisons some will differ by chance alone. This is a form of selective reporting, and an untiring assistant makes it easier, often without any intention to mislead.
Gelman and Loken (2014) described a subtler version of the same problem, which they called the garden of forking paths. Every analysis involves choices about which respondents to exclude, how to code variables, which subgroups to compare and which cut-offs to use. If those choices are made after looking at the data, a different dataset could have led to different choices, so even a single analysis can produce a statistically significant result that does not reflect a real pattern. The authors stressed that this can happen without any conscious fishing through the data, through ordinary and well-intentioned judgement applied after the results are visible. Interactive 8.1 shows the simpler case of many analyses: in a world in which no real differences exist, it estimates how often at least one of several analyses crosses the conventional threshold of p < 0.05.
Interactive 8.1: How many analyses before a false positive?
Each square is one simulated study in which no real difference exists. The analyst tries several analyses per study and reports a "finding" if any of them gives p < 0.05. When there is no real difference, a correctly computed p-value is equally likely to fall anywhere between 0 and 1. Move the slider and run the simulation.
Expected share of studies with at least one false positive (independent analyses): 5.0 percent
Press the button to simulate.
With one analysis per study, about 5 percent of studies report a false positive, which is the rate the threshold is designed to allow. With 5 independent analyses the expected share is 22.6 percent, with 10 it is 40.1 percent and with 20 it is 64.2 percent. Real analyses of the same data are usually correlated, which slows this rise, but the direction is the same. The simulation shows only the deliberate version of the problem; Gelman and Loken's point is that choices made after seeing the data inflate the error rate even when only one analysis is run.
A pre-specified analysis plan
The protection against both problems is a pre-specified analysis plan, a short document written and dated before the results are seen. It states the question, the data and the rules for including respondents, the definition of each variable (including how missing-value codes and scale scores are handled), the groups and thresholds, the statistics to be reported, any tests and how they will be interpreted, and how any departure from the plan will be reported. HSCI 410 Lesson 1 shows how to write the plan down, with a date, before modelling begins. A plan has two further uses in AI-assisted work. It can be pasted into the prompt, so that the assistant's code follows the plan, and the assistant can be asked to compare a finished script with the plan and list any differences.
Box 8.8 sets out Jordan's plan for the age-group analysis: the question, the data, the scoring rule, the statistics to be reported and the decision to run no tests.
Box 8.8: Jordan's plan, written before step 6 was run
Question: the percentage of 2021 CSCS respondents scoring 6 or higher on the three-item UCLA Loneliness Scale, in four age groups (16 to 29, 30 to 44, 45 to 64, 65 and older). Data: all 4,045 respondents in the 2021 wave; respondents without a complete score are excluded and counted. Scoring: items scored 1 to 3 and added; "Presented but no response" treated as missing. Statistics: number, mean, standard deviation and percentage scoring 6 or higher in each group; two plots. No statistical tests; no other subgroups. Any further analysis requested later will be reported as additional to this plan.
Box 8.9 asks the reader to decide how Jordan should respond when the assistant offers to run many further comparisons.
After producing Jordan's table, the assistant adds: "Would you like me to test whether loneliness differs by province, gender, relationship status, income, employment, education and eight other variables, and report the strongest associations?" Decide how Jordan should respond, and explain why with reference to Box 8.8 and to the garden of forking paths.
Jordan should decline the offer as an addition to this analysis. The plan specified four age groups, descriptive statistics and no tests, and the team lead's question concerned age. Fourteen further comparisons, with the strongest reported, would almost guarantee at least one apparently significant association even if none were real: with 14 independent tests at the 0.05 level, the chance of at least one false positive is about 51 percent. If the team later needs to know whether loneliness differs by, for example, income, Jordan can write a new plan for that question before running it, ask the epidemiologist which variables and methods suit it, and report it as a separate analysis. The assistant's offer is a typical example of how easy AI makes it to multiply analyses, which is why the decision belongs with the analyst.
Jordan's analysis now rests on a plan, a cleaned file, a table confirmed by a second method and two plots. Every step used only public, de-identified survey data described through a dictionary. Section 4 turns to the rules that apply when the data are confidential.
Before that, the reflection and knowledge check that follow review the four checks, the two silent errors and the case for a pre-specified analysis plan.
Reflection
An assistant wrote R code that computed the mean nightly sleep of 2,000 survey respondents as 10.6 hours. The data dictionary says that sleep_hours ranges from 0 to 24 and that the value 99 means "Don't know". In fact, 80 respondents answered Don't know, and the other 1,920 respondents have a mean of 6.9 hours. (a) Describe three checks that would reveal the problem, and say what each one would show. (b) Explain how to correct the code and what the corrected result should be. (c) The team then asks you to "find which groups sleep least", using twelve available demographic variables, with a test for each. Explain the risk in this request and how you would respond.
(a) A range check with summary(sleep_hours) would show a maximum of 99, which is impossible for hours in a night. Counting values with table() or sum(sleep_hours == 99) would show 80 respondents with the code. A second method, such as computing the mean after excluding values above 24, would give 6.9 hours, far from 10.6, and a histogram would show a separate spike at 99. A known-answer test on four invented rows, one of them coded 99, would show the code including 99 in the mean.
(b) The fix converts the code to missing before averaging, for example mutate(sleep_hours = na_if(sleep_hours, 99)), followed by mean(sleep_hours, na.rm = TRUE). The corrected mean is 6.9 hours for 1,920 respondents, and the report should state that 80 Don't know answers were excluded. The check is consistent: (1,920 × 6.9 + 80 × 99) / 2,000 = 10.6, which reproduces the wrong figure.
(c) Twelve tests with no real differences would give at least one p-value below 0.05 about 46 percent of the time if the tests were independent, and choosing comparisons after looking at the data adds the forking-paths problem. I would ask what decision the result will inform, write a dated plan that names the groups and the statistics in advance, and report any further comparisons as exploratory.
Minimum 20 characters required.
Question 1: The assistant's scoring code produced UCLA scores of 10 and 12 on a scale that runs from 3 to 9. What caused this?
Question 2: Of the 41 respondents scored wrongly by the assistant's code, 22 received scores between 6 and 9. What does this show about checking?
Question 3: The assistant built age groups with cut(age, breaks = c(16, 30, 45, 65, Inf), labels = age_labels). With the default settings of cut(), where do respondents aged exactly 65 end up?
Question 4: Which statement about the garden of forking paths (Gelman and Loken, 2014) is accurate?
Data Governance for AI-Assisted Analysis
Learning Objectives for this section
- Explain why identifiable or confidential health data must not be uploaded to consumer AI services.
- Classify data by sensitivity and match each class to the kinds of AI assistance that may be used with it.
- Describe the conditions that Population Data BC attaches to data access and what they imply for AI-assisted analysis.
- Use data dictionaries, synthetic data and local models to obtain AI help with code without sharing confidential records.
- Apply the OCAP® principles to a proposal to use AI tools with First Nations data.
Jordan's analysis used a public, de-identified survey file and described it to the assistant through a data dictionary. Much of the analysis done in public health uses data of a different kind: clinic and program records, surveillance and laboratory data, linked administrative data and research data collected under consent. Personal and confidential data of this kind come with legal duties, consent terms and agreements that decide who may see them and where they may be stored, and those rules apply to AI tools as they apply to any other software. This section sets out the reasons behind the rules, the conditions that a research data platform attaches to access, the ways an analyst can still obtain AI help with code, and the principles that govern First Nations data.
Why identifiable and confidential data stay out of consumer AI services
Personal health information is information about an identifiable person's health or health care, such as a diagnosis, a clinic visit or a screening score linked to a name, a health number or a combination of details that points to one person. Confidential data include personal information of other kinds, research data collected under consent, data shared under an agreement and unpublished work belonging to others. Lesson 3 taught students to classify information before entering it into any tool. For analysis, the classification decides which kinds of AI assistance may be used, as Figure 8.5 summarizes.
Several reasons support the rule that identifiable and confidential data stay out of consumer AI services, and each applies even when the analyst means only to fix a line of code. The first concerns control: text and files entered into a consumer service are stored by the provider under its terms of service, which Lesson 3 showed can differ markedly from the terms of an institutional tool in how long inputs are kept and whether they may be reviewed or used to improve models. The second is legal. In British Columbia, the Freedom of Information and Protection of Privacy Act (FIPPA) governs personal information held by public bodies, including health authorities. Since amendments in 2021, a public body may disclose personal information for storage outside Canada only in accordance with the regulations, and a new program or system that stores sensitive personal information outside Canada requires a privacy impact assessment, an assessment that no individual's improvised use of a consumer chatbot has received. The third concerns consent and agreements: research participants agree to particular uses of their data by particular people, and ethics approvals and data agreements specify who may access the data and where. The fourth is that de-identified is not the same as anonymous. Removing names does not prevent identification when a combination of details, such as age, sex, community and a rare condition, points to one person, which is a particular risk in small rural communities such as many of those Cedar Valley serves.
Box 8.10 applies these reasons to the Cedar Valley pilot, both to Jordan's survey analysis and to a possible next analysis of the Connector program's own participant records.
The Cedar Valley population health team's enterprise assistant was procured after a privacy impact assessment and operates under the Responsible AI Policy Framework for the B.C. Health Sector (Ministry of Health, June 2026). Under the pilot's rules it may receive public documents, internal non-confidential documents and aggregate data, and it must never receive personal health information or other identifiable data. For Jordan's survey analysis, every step complied: the data were public and de-identified, and the assistant received only dictionary entries and output summaries. Suppose the team's next analysis used the Connector program's own participant records, which hold names, dates of birth, clinic, connector notes and UCLA scores at referral and follow-up. The assistant could then help with code written against the dictionary and tested on synthetic stand-ins, while the records themselves stayed on the health authority's secure network and the code ran there. Jordan's tested score_item() function from Section 3 would carry over unchanged.
The same reasons shape the conditions that research data platforms attach to access, and the next part describes them through the example of Population Data BC.
Research data platforms: the example of Population Data BC
Population Data BC (PopData) supports research access to linked administrative data in British Columbia, as HSCI 207 Lesson 1 explained. Its access process shows the conditions that research data platforms typically attach to sensitive data. According to PopData's guidance for researchers, research ethics and peer review must be completed before a data access request is submitted. Data stewards, the people or bodies responsible for each data source, review and approve or reject each request. The principal investigator and every team member who needs access must complete PopData's Privacy Training 101 for Researchers, and the team signs a research agreement and confidentiality pledges, with the data to be used only for the purpose requested. The analysis takes place inside PopData's secure research environment, which is reached only through an encrypted virtual private network with two-factor authentication; all imports into and exports from the environment are monitored, ethics approval must be kept current, and research materials must be submitted to the data stewards before public dissemination.
These conditions answer the question of AI use in practice. Record-level data cannot be moved from a secure environment into an outside AI service, since the agreement restricts their use and the platform monitors what leaves the environment. AI help with code therefore happens outside the environment, through the data dictionary and synthetic stand-ins, and the finished code is brought into the environment through the platform's own process. Unreleased output belongs under the same rules: an analyst who wants help interpreting a table should wait until the data stewards have approved its release, or describe its structure without its numbers. Any AI tool offered inside a secure environment by the platform itself is a different matter, governed by the platform's rules.
Keeping the records inside the secure environment still leaves several ways to obtain AI help with code, which the next part sets out.
Getting AI help without sharing the data
An analyst working with confidential data can still use an assistant for most of the coding work described in this lesson. The four approaches below can be combined, and all of them keep the records where the rules require them to be.
The data dictionary approach of Section 2 gives an assistant the names, types, permitted values and missing-value codes it needs, without a single record. For confidential datasets, the dictionary itself should be checked before it is shared, because a dictionary that lists counts for each category can reveal small groups. Variable names and codes are usually safe to share; frequencies for rare categories may not be.
Synthetic data are invented records that copy the structure of the real data: the same column names, types and codes, with made-up values. A synthetic stand-in lets an analyst share data with an assistant, get code working and reproduce an error without exposing anyone. The simplest stand-in, shown in Worked code 8.11, uses random values. More realistic synthetic data that preserve the statistical relationships in the real data can be generated with tools such as the R package synthpop (Nowok, Raab and Dibben, 2016); because such data are derived from real records, they can carry some disclosure risk and are subject to the data steward's approval.
A local model is an open-weight language model that runs on the analyst's own computer, for example through a desktop application such as Ollama, so that prompts and data never leave the machine. Local models remove the transfer of data to a provider. They are usually smaller than the models behind commercial assistants and make more errors in code, so every check in Section 3 still applies, and their use with confidential data still requires that the computer and the arrangement be approved for those data.
An organization can procure an assistant under terms it has negotiated, after a privacy impact assessment, as the Cedar Valley Health Authority did for its pilot. Such a tool may be approved for more kinds of data than a consumer service, but its approval has limits, and the limits are written into the rules for its use. Approval for aggregate data does not extend to records, and a user who is unsure which class a dataset belongs to asks the privacy officer before entering it.
Worked code 8.11 builds the simplest kind of synthetic stand-in: six invented respondents with the real column names and answer codes, and values drawn at random.
This block creates six invented respondents with the real column names and the real answer codes, including the missing-value code. The function sample() draws values at random from the options given, and set.seed() fixes the random draw so that the same six rows appear every time the script runs. No row corresponds to a real person, so the stand-in can be pasted into any assistant together with a question about code.
set.seed(488)
fake <- data.frame(
age = sample(16:100, 6, replace = TRUE),
gender = sample(c("Man", "Woman", "Non-binary",
"Presented but no response"), 6, replace = TRUE),
companionship = sample(c("Hardly Ever", "Some of the time", "Often",
"Presented but no response"), 6, replace = TRUE))
fake
age gender companionship
1 34 Non-binary Presented but no response
2 52 Non-binary Presented but no response
3 49 Man Often
4 53 Presented but no response Presented but no response
5 54 Non-binary Hardly Ever
6 38 Man Some of the time
The values are random, so the proportions mean nothing (three of the six "respondents" are non-binary, for example), and a stand-in of this kind is useful only for testing code. Its value lies in the codes: because "Presented but no response" appears in two columns, any code tested on these rows must deal with it, which is exactly the case that the assistant's scoring code mishandled in Section 3.
Each of these approaches keeps confidential records where the rules require them to be. Data about First Nations communities carry further obligations, set out in the principles of OCAP®, which the next part describes.
OCAP® and First Nations data
The First Nations principles of OCAP® were established in 1998 by First Nations leadership, and OCAP® is a registered trademark of the First Nations Information Governance Centre (FNIGC). The four principles set out how First Nations data are to be collected, protected, used and shared. HSCI 207 Lessons 4 and 5 introduced them in the context of engagement and research ethics, and they apply with particular force to AI tools, which move data and derived information across organizational and national boundaries.
The four flip cards that follow describe the principles of ownership, control, access and possession.
In practice, an analyst who works with First Nations data under a data sharing agreement asks the community's data governance body, through the channels the agreement names, before using any AI tool in the analysis, including tools that receive only a description of the data. A data sharing agreement can address AI explicitly by stating whether AI tools may be used, which ones, with what kinds of data, and how the community will see the code and the results. The safe default for an analyst is to treat any AI use that the agreement does not mention as unapproved until the community has decided. Lesson 10 extends these ideas with the CARE Principles for Indigenous Data Governance and relates them to the commitments in the B.C. framework.
Box 8.11 asks the reader to classify five requests from a health authority's analytics team under rules like those of the Cedar Valley pilot.
For each of the following requests from members of a health authority's analytics team, decide whether it is acceptable, acceptable with conditions, or not acceptable, under rules like those of the Cedar Valley pilot (an approved enterprise assistant for public, internal non-confidential and aggregate data; never personal health information or other identifiable data). (1) Pasting the data dictionary of the program's referral database, with variable names and codes but no counts, into the enterprise assistant to get help writing code. (2) Uploading a spreadsheet of 300 referral records with names removed but dates of birth and postal codes kept, to a free consumer chatbot, to find a bug. (3) Asking the enterprise assistant to help interpret a table of referral counts by clinic, in which two clinics have fewer than five referrals. (4) Running an open-weight model on a personal laptop to write code for the referral records. (5) Pasting a synthetic stand-in with the real column names into the enterprise assistant.
(1) Acceptable: a dictionary of names and codes contains no records, provided it lists no counts for small categories. (2) Not acceptable: the records are confidential and potentially identifiable through dates of birth and postal codes, and a consumer chatbot is outside the pilot entirely; a synthetic stand-in with the same columns would serve the same purpose. (3) Acceptable with conditions: aggregate data are permitted, but counts below five can identify people in small communities, so the small cells should be suppressed or combined before the table is shared. (4) Not acceptable as described: a local model keeps data on the machine, but a personal laptop is not an approved environment for program records; the analyst would need the privacy office's approval for both the model and the computer. (5) Acceptable: synthetic rows describe no real person, which makes this the standard way to get help with code for confidential data.
Box 8.12 draws the lesson together. Table 8.5 sets out, for six tasks in data analysis and coding in R, how AI performs and the check that applies.
Box 8.12: Strengths and limits: AI for data analysis and coding in R
Table 8.5. How AI performs at six tasks in data analysis and coding in R, and the check that applies to each.
| Task | How AI performs | The check that applies |
|---|---|---|
| Writing R code for a clearly described step | Fast and usually correct when the data are described through the dictionary; guesses names and codes when they are not | Run each step and confirm row counts and missing values |
| Explaining code and error messages | Strong: clear explanations at any level of detail | Test the explanation by running the code or a changed version of it |
| Handling missing-value codes and scoring rules | Weak unless told the codes; the code can run and still give wrong answers | Recompute by a second method and run a known-answer test |
| Grouping and filtering | Prone to errors at boundaries and to silent loss of rows | Check the youngest and oldest value in each group and count rows after each filter |
| Choosing and interpreting the analysis | Runs any analysis requested and offers more; tends to accept the user's framing | A pre-specified analysis plan and the methods taught in HSCI 410 |
| Working with confidential data | Consumer tools are not permitted; approved tools have stated limits | Classify the data first; use the dictionary, synthetic stand-ins or an approved local model |
An assistant allows a student with no programming experience to carry out a real analysis in R and to learn the language while doing it, and the checks in this lesson make the results dependable. A good next step is to rerun the lesson's script with one change, such as a threshold of 7 or two age groups in place of four, and to predict each output before running it.
This section has set out why identifiable and confidential data stay out of consumer AI services, the conditions that research data platforms attach to access, the ways to obtain help with code without sharing the data, and the principles that govern First Nations data. The reflection and knowledge check that follow review these rules.
Reflection
A health authority's population health team uses an enterprise AI assistant that was approved, after a privacy impact assessment, for public documents, internal non-confidential documents and aggregate data, and never for personal health information or other identifiable data. An analyst must compare loneliness scores at referral and at six months for 640 participants in a community program. The program records contain names, dates of birth, postal codes, clinic, notes written by staff, and three loneliness items at each time point. The records of 85 participants come from a First Nation whose data are shared with the health authority under a data sharing agreement that does not mention AI tools. The OCAP® principles are Ownership (the community owns its information collectively), Control (the community has the right to control research and information processes that affect it), Access (the community must have access to its data and decides who else may) and Possession (physical control of the data, the means by which ownership is protected). Explain what the analyst may and may not do with the assistant, how the analyst can still get help writing and testing the R code, and what the OCAP® principles require for the 85 participants' records.
The records are identifiable and contain personal health information, so none of them may be entered into the assistant, even with names removed: dates of birth and postal codes can identify people, and staff notes may contain anything. The analyst may give the assistant the data dictionary (variable names, types, codes and how the loneliness items are scored), provided it contains no counts for small categories, and may ask it for code in small steps. To test that code, the analyst can build a synthetic stand-in with the real column names and codes and invented values, including missing-value codes, and share it with the assistant. The code then runs on the real records on the health authority's secure network. Aggregate results, such as mean scores by time point with small cells suppressed, may be discussed with the assistant once they are approved for that use.
For the 85 First Nation participants, the agreement does not mention AI, so the analyst should treat any AI use as unapproved until the community's data governance body has decided. Control means the community decides whether AI tools are used, even for code that only touches its records. Possession means the records must not leave the agreed environment. Access means the community can see the code and the results, and Ownership means that findings about the community are reported as the agreement and the community direct.
Minimum 20 characters required.
Question 1: An analyst wants help fixing a bug and considers pasting 50 program records, with names removed but dates of birth and postal codes kept, into a free consumer chatbot. What should the analyst do?
Question 2: Which set of conditions does Population Data BC attach to data access, according to its guidance for researchers?
Question 3: Under the OCAP® principles, what does Possession refer to?
Question 4: Which statement about running an open-weight model locally on an analyst's computer is accurate?
Final Assessment
Bringing It All Together
This lesson followed one analysis from a data dictionary to a cleaned file, a descriptive table and two plots, with an AI assistant writing most of the R code and a student with no programming experience directing and checking it. The evidence on AI and programming speed showed why that division of labour matters. Gains are large on well-defined tasks of a familiar kind and can disappear when the user must review complex code, and people's sense of how much an assistant helped them is an unreliable guide.
The workflow kept every step small enough to read: describe the data through its dictionary, ask for one step and what to expect, run it, read the output, check it and save it. Two of the assistant's steps ran without any message and were wrong. A missing-value code was counted as an answer worth 4, and the age groups put 65-year-olds in the wrong group. A range check, a comparison with an independently computed score, a known-answer test and a look at the youngest and oldest age in each group found both errors, and the corrected code reproduced the survey team's own results exactly. A dated analysis plan then guarded against the easiest temptation that AI creates, which is to run many analyses and report the most favourable.
The same workflow carries over to confidential data with one change: the records stay where the rules require. Data dictionaries, synthetic stand-ins, approved institutional tools and local models give an analyst most of the benefit of AI help with code, while privacy law, data access agreements and the OCAP® principles decide where the data may go and who decides how they are used.
Key Takeaways from this lesson
- AI coding assistance comes in four main forms (chat assistants, editor assistants, coding agents and code-executing data tools) that differ in what they can see and in who runs the code.
- In a controlled experiment, programmers with an assistant finished a defined task 55.8 percent faster (Peng and colleagues, 2023), while in a randomized trial experienced developers on their own large projects took 19 percent longer and believed they had been faster (METR, 2025).
- The gains from AI coding tools depend on the task and on the user's ability to judge the code, so a beginner benefits most by reading, running and checking every step.
- An assistant knows only what it is told about the data, and the data dictionary is the most efficient way to tell it, without sharing a single record.
- Asking for code in small steps, with the expected output stated in advance, keeps each block short enough to read and makes errors easy to locate.
- Code that runs without an error message can still give a wrong answer, most often where the code meets missing-value codes, scoring rules and group boundaries.
- Four checks catch most silent errors: confirming row counts and missing values, recomputing by a second method, plotting the data and testing on data with a known answer.
- AI makes it easy to run many analyses, and choices made after seeing the data can produce false positives even in a single analysis, so a dated, pre-specified analysis plan comes before the results.
- Identifiable and confidential health data stay out of consumer AI services; dictionaries, synthetic stand-ins, approved tools and local models provide AI help with code while the records stay in place.
- The OCAP® principles give First Nations control over whether and how AI tools are used with their data, and an agreement that does not mention AI should be treated as not permitting it until the community decides.
Core Concepts Reviewed
Section 1: forms of AI coding assistance, what an assistant can see, the controlled experiment of Peng and colleagues (2023), the METR (2025) randomized trial, the gap between perceived and measured speed, and the jagged frontier of AI help with R.
Section 2: the six-step workflow, describing data through a data dictionary, small steps with expected output, reading R notation, warnings and errors, asking for explanations, scripts, Quarto documents and the AI use log.
Section 3: silent errors, the four checks, a missing-value code counted as a number, boundary errors in cut(), second methods and known-answer tests, descriptive tables and plots, the analyst's responsibility for the analysis, the garden of forking paths and the pre-specified analysis plan.
Section 4: personal health information and data classification, reasons to keep confidential data out of consumer AI services, FIPPA, the access conditions of Population Data BC, synthetic data, local models, approved institutional tools and the OCAP® principles.
The final reflection asks you to bring the lesson's workflow and checks together for a public dataset and to adapt them for confidential records.
Reflection
Describe, step by step, how you would use an AI assistant to produce a descriptive table and one plot in R from a public survey file, from the first prompt to the saved script, and name the check you would apply at each step. Assume the file has a data dictionary, a text code for questions left blank, a scale score already computed by the survey team, and an age variable that you need to group into four age groups. Then explain which parts of your workflow would change if the data were confidential program records, and which parts would stay the same.
My first prompt describes the data through its dictionary entries, states the question and asks for a plan and the code for step 1 only. Step 1 loads the data and keeps the rows I need; I check the row count against the dictionary. Step 2 selects and renames the variables, and glimpse() confirms names and types. Step 3 converts the blank-answer code to NA, and the count of missing values must rise by exactly the number of codes. Step 4 computes my own score from the items with an explicit rule; I compare it with the survey team's score as a second method and run a known-answer test on a few invented rows. Step 5 creates the age groups; I check the youngest and oldest age in each group and confirm that no one is left without a group. Before step 6 I write a dated plan naming groups and statistics. Step 6 produces the table, confirmed by a second method, and the plot, inspected for impossible values. I save the script, rerun it from a fresh session and log the assistant's part.
With confidential records, the steps and checks would stay the same. What would change is where the data go: no record would enter the assistant, the dictionary would be checked for small counts, code would be tested on a synthetic stand-in, the analysis would run in an approved environment, and any First Nations data would need the community's approval for AI use.
Minimum 30 characters required.
Final Knowledge Assessment
Question 1: Which statement best summarizes what the two productivity studies in this lesson show when read together?
Question 2: An assistant proposes ten lines of R that run without any warning or error. What is the most dependable next action?
Question 3: Jordan's table showed that 51.7 percent of respondents aged 65 and older scored 6 or higher. Which sentence reports this accurately?
Question 4: Which of the following is a missing-value code?
Question 5: In the corrected age-group code, what does the argument right = FALSE change?
Question 6: Which form of coding assistance is most likely to hide the code behind its answer unless the user asks to see it?
Question 7: Why is a known-answer test useful for the score_item() function even after the comparison with the survey's own score passed?
Question 8: An analyst plans to compare loneliness across 14 variables and report the strongest association. If no real associations exist and the tests are independent, what is the approximate chance of at least one p-value below 0.05?
Question 9: Which of the following belongs in a pre-specified analysis plan?
Question 10: Which approach lets an analyst get AI help with code for confidential program records while keeping the records in place?
Question 11: Under British Columbia's Freedom of Information and Protection of Privacy Act, as amended in 2021, what applies when a public body stores sensitive personal information outside Canada through a new program or system?
Question 12: Which OCAP® principle most directly holds that a First Nation decides whether AI tools may be used in research that affects it?
Question 13: In Figure 8.3, 23.1 percent of older respondents scored 3 and 14.8 percent scored 9, compared with 12.5 and 7.9 percent of younger respondents. What does this add to the table of means?
Question 14: A student wants to understand a ten-line R script written by an assistant. Which prompt is most likely to build the student's own understanding?
Question 15: Which statement about the METR (2025) trial is accurate?
Glossary: Key Terms, People & Frameworks
📚 Reference page, available throughout the lesson
Key terms, tools and people from this lesson on using AI assistants to write, understand and check analyses in R.
nrow(cscs).
|>, which passes the result on its left into the function on its right; it can be read as "and then".