Module 2 — Test Bank
Instructions
Each practice test consists of four questions, one from each type below. Download the dataset, do the work in R (in Positron), and check your answers by expanding the answer section below each question.
On the real test, you will receive a PDF with four questions and will submit a single .R script that reads the dataset, performs the analysis, and prints the answers to the console. Your script must run top-to-bottom from a fresh session without errors. You will have access to the full test bank for practice — the test draws one question randomly from each type.
A few expectations for every script you submit:
- Start the script with a header comment (your name, the date, what the script does).
- Load any libraries you use (
library(tidyverse)) at the top. - Read the data with a relative path, assuming the CSV is in the same folder as the script.
- Use formulas/functions, not hard-coded numbers — e.g.
mean(wheat$yield_bu_ac, na.rm = TRUE), not45.33.
Dataset
Download: sask_wheat_2025.csv — 150 spring-wheat fields in Saskatchewan in 2025.
| Column | Description |
|---|---|
field_id |
Unique field identifier (W001–W150) |
soil_zone |
Brown, Dark Brown, or Black |
variety |
AAC Brandon, CDC Landmark, or AAC Viewfield |
acres |
Field size in acres |
yield_bu_ac |
Yield in bushels per acre (4 values are missing — coded as NA) |
protein_pct |
Grain protein content, percent (3 values are missing) |
seeded_rate_lb_ac |
Seeding rate in pounds per acre |
This dataset deliberately contains a few missing values in yield_bu_ac and protein_pct. Watch for them — several questions depend on handling NA correctly.
Read the file with read_csv() from the tidyverse:
library(tidyverse)
wheat <- read_csv("sask_wheat_2025.csv")read_csv() reads blank cells as NA. Confirm the column types it guessed look right (yield_bu_ac and protein_pct should be numeric, dbl).
Type 1: Reading Data and Exploring
These questions check that you can get a dataset into R and inspect it. Your script should read the CSV and then print the answers. Use print() (or just the bare object) so the answers appear in the console when the script runs.
Variant 1A
(a) Read sask_wheat_2025.csv into a data frame called wheat.
(b) How many rows does the dataset have? How many columns? Use functions, not manual counting.
(c) Print the names of all the columns.
(d) Print the first 6 rows of the data.
(e) Run summary(wheat). From its output, what is the mean yield that summary() reports, and how does summary() indicate the missing values in yield_bu_ac?
Answers
library(tidyverse)
wheat <- read_csv("sask_wheat_2025.csv") # (a)
nrow(wheat) # (b) 150 rows
ncol(wheat) # (b) 7 columns
names(wheat) # (c)
head(wheat) # (d) first 6 rows (W001–W006)
summary(wheat) # (e)| Part | Answer |
|---|---|
| (b) | 150 rows, 7 columns |
| (c) | field_id, soil_zone, variety, acres, yield_bu_ac, protein_pct, seeded_rate_lb_ac |
| (d) | First six fields are W001–W006 |
| (e) | summary() reports a mean yield of about 45.33, and shows NA's : 4 in the yield_bu_ac column |
Note: summary() automatically ignores NAs when computing the mean, which is why it still returns a number. This is different from calling mean() directly (see Type 3).
Common mistakes:
- Using
read.csv()(base R) instead ofread_csv()(tidyverse). Both work, but the course standard isread_csv(), and it reports column types. - Counting rows or columns by eye instead of with
nrow()/ncol(). - Hard-coding the answer (
150) instead of letting the function compute it.
Variant 1B
(a) Read sask_wheat_2025.csv into a data frame called wheat.
(b) Print the last 6 rows of the data.
(c) How many distinct soil zones appear in the data? List them. (Hint: unique() or n_distinct().)
(d) How many fields are missing a yield_bu_ac value? (Hint: sum(is.na(...)).)
(e) What variety was planted in field W050? Extract it with code, do not scroll through the data.
Answers
library(tidyverse)
wheat <- read_csv("sask_wheat_2025.csv")
tail(wheat) # (b) last 6 rows (W145–W150)
unique(wheat$soil_zone) # (c)
n_distinct(wheat$soil_zone) # (c) 3
sum(is.na(wheat$yield_bu_ac)) # (d) 4
wheat$variety[wheat$field_id == "W050"] # (e)
# tidyverse alternative:
wheat |> filter(field_id == "W050") |> pull(variety)| Part | Answer |
|---|---|
| (c) | 3 zones: Brown, Dark Brown, Black |
| (d) | 4 fields are missing a yield value (W017, W058, W096, W133) |
| (e) | W050 was planted with AAC Brandon |
Common mistakes:
- Using
length(unique(...))is fine, butn_distinct()is the tidyverse idiom and handlesNAmore cleanly. - Forgetting that
is.na()returns a logical vector — you needsum()around it to get a count. - For (e), using
==to filter a string requires the value in quotes:"W050", notW050.
Type 2: Vectors and Functions
These questions test the building blocks: creating vectors, element-wise arithmetic, and calling functions. You do not need the CSV for most of these — you create the vectors in your script.
Variant 2A
(a) Create a vector called v containing the five values 48, 52, 47, 55, 50.
(b) Compute the mean, median, and sample standard deviation of v.
(c) What does v * 2 return? What does length(v) return?
(d) Create a second vector b <- c(10, 20, 30) and a vector a <- c(1, 2, 3). What is a + b? Explain in one sentence what “element-wise” means.
(e) Using the wheat dataset, extract the acres column as a vector and compute its mean.
Answers
v <- c(48, 52, 47, 55, 50) # (a)
mean(v) # (b) 50.4
median(v) # (b) 50
sd(v) # (b) 3.21 (sample sd)
v * 2 # (c) 96 104 94 110 100 — each element doubled
length(v) # (c) 5
a <- c(1, 2, 3)
b <- c(10, 20, 30)
a + b # (d) 11 22 33
mean(wheat$acres) # (e) about 338.76| Part | Answer |
|---|---|
| (b) | mean = 50.4, median = 50, sd ≈ 3.21 |
| (c) | v * 2 = 96 104 94 110 100; length(v) = 5 |
| (d) | a + b = 11 22 33 |
| (e) | mean acres ≈ 338.76 |
Part (d) explanation: “Element-wise” means the operation is applied to each pair of corresponding positions: position 1 with position 1, position 2 with position 2, and so on. R does this automatically without a loop.
Common mistakes:
- Using
sd()and expecting the population standard deviation — R’ssd()is always the sample sd (divides by n − 1). There is no built-in population sd. - Writing
c(48 52 47 55 50)(spaces) instead ofc(48, 52, 47, 55, 50)(commas). - For (e),
acreshas no missing values, sona.rmis not required here — but it never hurts to include it.
Variant 2B
(a) Create a vector prices <- c(8.50, 9.10, 8.95, 9.40, 8.75) (price per bushel, in dollars).
(b) Compute the sum and the maximum of prices.
(c) Create a vector bushels <- c(1200, 1500, 1100, 1800, 1350). Compute total revenue as the element-wise product of prices and bushels, summed. (Hint: sum(prices * bushels).)
(d) What does quantile(bushels, 0.25) return, and what does it mean?
(e) Using the wheat dataset, extract the seeded_rate_lb_ac column and compute its minimum and maximum.
Answers
prices <- c(8.50, 9.10, 8.95, 9.40, 8.75) # (a)
sum(prices) # (b) 44.70
max(prices) # (b) 9.40
bushels <- c(1200, 1500, 1100, 1800, 1350)
sum(prices * bushels) # (c) total revenue
quantile(bushels, 0.25) # (d)
min(wheat$seeded_rate_lb_ac) # (e)
max(wheat$seeded_rate_lb_ac) # (e)| Part | Answer |
|---|---|
| (b) | sum = 44.70, max = 9.40 |
| (c) | sum(prices * bushels) = 8.50×1200 + 9.10×1500 + 8.95×1100 + 9.40×1800 + 8.75×1350 = $58,807.50 |
| (d) | The first quartile of bushels = 1200 — 25% of the values are at or below it |
| (e) | seeding rate ranges from about 90 to 140 lb/ac |
Part (c) note: This is the same idea as Excel’s SUMPRODUCT. prices * bushels multiplies element-by-element, and sum() adds the results.
Common mistakes:
- Computing
sum(prices) * sum(bushels)instead ofsum(prices * bushels). The first multiplies the totals (wrong); the second multiplies row-by-row then totals (right). - Passing a percentage (25) to
quantile()instead of a proportion (0.25).
Type 3: Summary Statistics in R (and Missing Values)
These questions use the wheat dataset and focus on computing statistics in R — and handling the missing values correctly. Remember: if a column contains NA, functions like mean() and sd() return NA unless you pass na.rm = TRUE.
Variant 3A
(a) Compute the mean of yield_bu_ac. What happens if you forget na.rm = TRUE? Show both, and explain in one sentence why they differ.
(b) Compute the median, sample standard deviation, and sample variance of yield_bu_ac (ignoring missing values).
(c) Compute the first and third quartiles (Q1, Q3) and the IQR of yield_bu_ac.
(d) Compute the 90th percentile of yield_bu_ac. Write one sentence explaining what it means.
(e) Compute the mean protein_pct, ignoring missing values.
Answers
mean(wheat$yield_bu_ac) # (a) NA — because there are 4 NAs
mean(wheat$yield_bu_ac, na.rm = TRUE) # (a) 45.33
median(wheat$yield_bu_ac, na.rm = TRUE) # (b) 45.05
sd(wheat$yield_bu_ac, na.rm = TRUE) # (b) 7.34
var(wheat$yield_bu_ac, na.rm = TRUE) # (b) 53.81
quantile(wheat$yield_bu_ac, 0.25, na.rm = TRUE) # (c) Q1 ≈ 40.23
quantile(wheat$yield_bu_ac, 0.75, na.rm = TRUE) # (c) Q3 ≈ 50.95
IQR(wheat$yield_bu_ac, na.rm = TRUE) # (c) ≈ 10.73
quantile(wheat$yield_bu_ac, 0.90, na.rm = TRUE) # (d) ≈ 54.55
mean(wheat$protein_pct, na.rm = TRUE) # (e) ≈ 15.43| Part | Value |
|---|---|
(a) without na.rm |
NA |
(a) with na.rm = TRUE |
45.33 bu/ac |
| (b) median | 45.05 bu/ac |
| (b) sample sd | 7.34 bu/ac |
| (b) sample variance | 53.81 (bu/ac)² |
| (c) Q1 / Q3 | 40.23 / 50.95 bu/ac |
| (c) IQR | 10.73 bu/ac |
| (d) P90 | 54.55 bu/ac |
| (e) mean protein | 15.43% |
Part (a) explanation: Without na.rm = TRUE, mean() returns NA because R cannot average a set that contains an unknown value — any arithmetic involving NA is NA. Passing na.rm = TRUE drops the missing values first, then averages the rest.
Part (d) interpretation: 90% of fields (with a recorded yield) yielded 54.55 bu/ac or less; only the top 10% exceeded it.
Common mistakes:
- Forgetting
na.rm = TRUEand reportingNAas the answer. If a summary function returnsNA, missing values are the first thing to check. - Using
var()and forgetting the units are squared (bu/ac)². quantile()returns a named value (e.g.90%). That is fine — the number is what matters.
Variant 3B
(a) Compute the mean yield for each soil zone (Brown, Dark Brown, Black), ignoring missing values. Which zone has the highest mean yield? (You may use base R subsetting or a group_by()/summarise() pipe.)
(b) Compute the mean yield for each variety. Which variety yields the most on average?
(c) How many fields have a yield strictly greater than 50 bu/ac? (Remember the NAs — they should not be counted.)
(d) Compute the range (min and max) of yield_bu_ac, ignoring missing values.
(e) Run summary() on just the yield_bu_ac and protein_pct columns. How many NAs does each report?
Answers
# (a) and (b) — the tidyverse way (you will master this in Module 3):
wheat |>
group_by(soil_zone) |>
summarise(mean_yield = mean(yield_bu_ac, na.rm = TRUE),
n = n()) |>
arrange(desc(mean_yield))
wheat |>
group_by(variety) |>
summarise(mean_yield = mean(yield_bu_ac, na.rm = TRUE)) |>
arrange(desc(mean_yield))
# (c) — count yields above 50; na.rm so NAs are not miscounted:
sum(wheat$yield_bu_ac > 50, na.rm = TRUE)
# (d):
range(wheat$yield_bu_ac, na.rm = TRUE)
# (e):
summary(wheat[, c("yield_bu_ac", "protein_pct")])Part (a) — mean yield by soil zone:
| Soil zone | Mean yield (bu/ac) | n |
|---|---|---|
| Black | 52.95 | 42 |
| Dark Brown | 45.17 | 58 |
| Brown | 38.58 | 46 |
Black soil has the highest mean yield, which matches the agronomy — Black soil zones are the most productive in the Prairies.
Part (b) — mean yield by variety:
| Variety | Mean yield (bu/ac) |
|---|---|
| CDC Landmark | 47.60 |
| AAC Brandon | 44.36 |
| AAC Viewfield | 43.83 |
CDC Landmark yields the most on average.
| Part | Answer |
|---|---|
| (c) | 39 fields yield above 50 bu/ac |
| (d) | range = 30.2 to 67.3 bu/ac |
| (e) | yield_bu_ac: 4 NAs; protein_pct: 3 NAs |
Common mistakes:
- For (c), omitting
na.rm = TRUEmakessum(...)returnNA, becauseNA > 50isNA, and summing anything withNAgivesNA. - Reporting
group_by()results withoutna.rm = TRUEinsidemean()— any group containing a missing yield would come backNA.
Type 4: Scripting, Reproducibility, and AI
These are short-answer / conceptual questions. On the real test you would type your answers as comments inside your .R script, or in a short text block. There is no single correct wording — the answers below show what a full-credit response covers.
Variant 4A
(a) Give two reasons we use R in addition to Excel, and one situation where Excel is still the better choice.
(b) Explain what the <- operator does and what c() does.
(c) Your colleague says: “I changed the CSV file in Excel, then re-ran my R script and got a different answer.” Explain why this is a reproducibility problem and how to avoid it.
(d) Describe one task where an AI coding assistant would genuinely help you, and one situation where relying on it could lead you astray.
Answers
(a) Any two of: R analyses are reproducible (the script is a permanent, re-runnable record); R scales to datasets far larger than Excel’s ~1M-row limit; R is version-controllable (plain text, meaningful diffs); R is composable (apply the same code to many files); R has far stronger statistical and modelling tools. Excel is still better for quick ad-hoc calculations, building interactive workbooks for non-technical colleagues, and presenting formatted tables.
(b) <- is the assignment operator — it stores a value in a variable (e.g. x <- 5 puts 5 into x). c() combines individual values into a single vector (e.g. c(1, 2, 3)).
(c) The problem is that the data was edited outside the script, so the script is no longer a complete record of the analysis — re-running it will not reproduce the colleague’s result, because the manual Excel edits aren’t captured anywhere. The fix is to do all data cleaning in the script itself (read the raw, unedited CSV, then transform it in R) so that the raw data plus the script fully determine the output. Never hand-edit the source data.
(d) Helps: scaffolding a script from a description, explaining an error message, suggesting the name of a function you can’t recall, or writing tedious boilerplate. Leads astray: it confidently assumes column names/types that don’t match your actual data, may use outdated package syntax, can write code that runs cleanly but answers the wrong question, and can introduce subtle bugs (silently dropping rows, approximate instead of exact matching). The safeguard is to read and understand every line before running it.
Variant 4B
(a) List three habits of a well-written R script (from the “Writing Good R Scripts” section).
(b) A script computes mean(wheat$yield_bu_ac) and gets NA. What is the most likely cause, and what is the one-argument fix?
(c) Explain what it means for a script to be “re-runnable from scratch,” and why the course insists on it.
(d) The course policy allows AI on assignments but not on tests. In one or two sentences, explain the reasoning behind that policy.
Answers
(a) Any three of: start with a header comment (name, date, purpose, inputs, outputs); load libraries at the top; use comments to explain why, not what; use descriptive variable names; break long operations into named steps; make the script re-runnable from scratch; test on a subset before running on the full data.
(b) The column almost certainly contains missing values (NA), and mean() propagates NA by default. The fix is to add na.rm = TRUE: mean(wheat$yield_bu_ac, na.rm = TRUE).
(c) “Re-runnable from scratch” means that if you clear your environment, restart R, and run the script top-to-bottom, you get exactly the same results — without clicking anything or running commands in a special order outside the script. The course insists on it because it is the operational definition of reproducibility: a result no one can reproduce (including future-you) cannot be trusted or built upon.
(d) AI is a real part of professional data work, so assignments let you practise using it responsibly — but the understanding has to be yours. Tests are AI-free precisely to verify that you, not the tool, can actually read data, compute statistics, and reason about results.
Auto-Generated Practice Quiz
For a randomized practice quiz that draws one question from each type, visit the Practice Quiz Generator. Each time you click Generate quiz, you get a fresh set of four questions — just like the real module test.