What this quiz covers
This quiz focuses on Explaining Analysis Steps, giving you a quick way to practice the rules, question types, and explanations that matter most for R Programming.
Assume sales contains no missing values. An analyst executes:
sales |> group_by(region) |> mutate(z_sales = (sales - mean(sales)) / sd(sales)) |> arrange(desc(z_sales)) |> ungroup()
Which statement would best document the transformation applied to sales?
R Programming Quiz
Practice Explaining Analysis Steps in R Programming with focused quiz questions that help you check what you know, review explanations, and build confidence with test-style prompts.
This quiz focuses on Explaining Analysis Steps, giving you a quick way to practice the rules, question types, and explanations that matter most for R Programming.
Try each quiz question before looking at the correct answer. Use the explanations to review missed ideas, then come back to similar questions until the pattern feels familiar.
Assume sales contains no missing values. An analyst executes:
sales |> group_by(region) |> mutate(z_sales = (sales - mean(sales)) / sd(sales)) |> arrange(desc(z_sales)) |> ungroup()
Which statement would best document the transformation applied to sales?
group_by(region) partitions the data by region before anything else runs. That means mutate(z_sales = (sales - mean(sales)) / sd(sales)) computes the mean and standard deviation within each region separately, not across all rows globally. This is the z-score formula z=σx−μ, applied group-wise. Next, arrange(desc(z_sales)) sorts all rows from highest to lowest standardized score — it doesn't average or aggregate anything. Finally, ungroup() removes the grouping structure. That sequence maps precisely onto answer C: within-region standardization → row-level sort by those scores → ungrouped result.
Answer A is wrong on two counts: the mean and SD are regional, not global, and arrange sorts individual rows, not regions by their averages. Answer B invents a ranking step that never appears in the code — mutate with the z-score formula is a direct transformation, not a rank-based one. Answer D reverses the actual order of operations; sorting happens after standardization, not before, and the grouping is by region, not established by row order.
A useful strategy: when you see group_by() followed by mutate() (rather than summarise()), remember that the calculation stays row-level but uses group-local statistics. If it were summarise(), rows would collapse. That distinction — mutate preserves rows, summarise collapses them — is a frequent exam focus in tidyverse questions.An analyst intends to assign performance labels but writes:
mutate(results, label = case_when(score >= 80 ~ 'Pass', score >= 90 ~ 'Honors', is.na(score) ~ 'Missing', TRUE ~ 'Review'))
Which explanation accurately describes the labels actually assigned by this code?
Honors, scores from 80 through 89 receive Pass, and missing scores receive Missing.Pass, including scores of at least 90; missing scores receive Missing, and lower scores receive Review. (correct answer)Pass, scores of at least 90 receive both matching labels, and missing scores receive Review.Pass, scores below 80 receive Review, and missing scores remain unlabeled because comparisons return missing values.case_when() in R, the critical rule is first-match wins — conditions are evaluated top to bottom, and once a row satisfies a condition, it gets that label and stops. No further conditions are checked. This question tests whether you understand that ordering determines outcomes.
Looking at the code, score >= 80 appears before score >= 90. A score of 95, for example, satisfies score >= 80 first, so it receives 'Pass' immediately — the Honors condition is never reached for that row. This means every score of 80 or above gets labeled 'Pass', and the Honors branch is effectively dead code. After that, is.na(score) handles missing values, and TRUE (the catch-all) assigns 'Review' to anything remaining — scores below 80.
This confirms B is correct: scores ≥ 80 (including those ≥ 90) all receive 'Pass', missing scores receive 'Missing', and lower scores receive 'Review'.
A is wrong because it describes the intended behavior, not the actual behavior — it assumes Honors is checked before Pass, but the order is reversed in the code. C is wrong on two counts: rows never receive multiple labels in case_when(), and the missing value logic is misapplied. D is partially right that scores below 80 get 'Review', but wrong that NA scores go unlabeled — is.na(score) explicitly catches them before the TRUE fallback.
Your study tip: whenever you read a case_when() block, mentally trace through conditions in order from top to bottom. Overlap between conditions is almost always a bug, and exams love testing whether you can spot it.An employee data frame is processed as follows:
employees |> group_by(department) |> slice_max(order_by = score, n = 1, with_ties = TRUE) |> ungroup()
Which explanation most accurately describes the output when some departments have tied highest scores?
ungroup() removes department boundaries before selection.slice_max() in dplyr, the key parameter to focus on is with_ties. By default — and explicitly here — with_ties = TRUE tells R to return all rows that share the maximum value, not just one arbitrarily chosen row. The grouping structure means this tie-breaking logic applies within each department separately, so if three employees in Marketing all score 95 (the department max), all three are returned.
This confirms B as correct. The pipeline groups by department, finds the highest score within each group, and because with_ties = TRUE, every employee matching that peak score is included. The final ungroup() simply removes the grouping metadata from the result — it has no effect on which rows were selected.
A is wrong because it describes behavior you'd get with with_ties = FALSE, which enforces a single row per group (using original row order as a tiebreaker). That's a different argument value entirely. C contains a subtle but important misconception: ungroup() is called after slice_max(), so it cannot retroactively change what was selected. The selection already happened within department boundaries; ungroup() just cleans up the result. D misreads how n = 1 works — it doesn't mean "consume one slot per tied employee." It defines how many distinct rank positions to include, so n = 1 always means first place only, regardless of how many employees share it.
A good rule of thumb: whenever you see with_ties in a slice_* function, ask yourself whether the question is testing the single-row versus all-tied-rows behavior — that distinction is a frequent exam trap.An analyst fits lm(log(price) ~ age, data = homes). The estimated coefficient of age is −0.08, where age is measured in years.
Which statement best explains the coefficient on the original price scale without overstating its meaning?
lm(log(price) ~ age), you need to mentally "undo" the log transformation to interpret coefficients on the original price scale. The model says log(price^)=β0+β1⋅age, so a one-unit increase in age adds β1 to the log of price — which means price gets multiplied by eβ1 on its original scale.
With β^1=−0.08, a one-year age increase multiplies fitted price by e−0.08≈0.923, a reduction of roughly 7.7%. Answer A captures this correctly and uses careful language ("associated with") that avoids claiming causation — important when interpreting observational regression output.
B is wrong on two counts: multiplying by 100 only works as a rough approximation for very small coefficients, and saying "exactly 8%" overstates precision. The true multiplicative factor is e−0.08, not 1−0.08. C confuses the log-scale coefficient with a raw-scale subtraction — subtracting 0.08 currency units makes no sense here because the model was built on log(price), not price itself. D uses e+0.08 instead of e−0.08, reversing the sign and implying price increases with age, which contradicts the negative coefficient entirely.
A useful habit: whenever you see log(y) on the left side of a formula call, always exponentiate the coefficient before interpreting it, and describe changes as multiplicative, not additive.An analyst runs the following pipeline on flights, which has one row per scheduled flight:
flights |> filter(!is.na(dep_delay)) |> mutate(delayed = dep_delay > 15) |> group_by(carrier) |> summarise(rate = mean(delayed), n = n())
Which explanation most accurately describes how rate and n are produced?
rate and n are calculated from every scheduled flight for each carrier.rate is the proportion delayed among remaining flights, and n is their count for each carrier. (correct answer)rate is their average delay, and n is their count for each carrier.filter(!is.na(dep_delay)) runs first, permanently dropping every row where dep_delay is missing. Only flights with a recorded departure delay remain. Next, mutate(delayed = dep_delay > 15) creates a logical column — TRUE if the delay exceeded 15 minutes, FALSE otherwise — for every surviving row. After grouping by carrier, summarise(rate = mean(delayed), n = n()) computes two things: because mean() on a logical vector returns the proportion of TRUE values, rate is the fraction of non-missing flights that were delayed more than 15 minutes; n is simply the count of those remaining rows per carrier. That makes B the accurate description.
A is wrong because filter(!is.na(dep_delay)) does not treat missing delays as "not delayed" — it removes those rows entirely before any calculation happens. C misreads mutate(delayed = dep_delay > 15) as a filter that keeps only delayed flights; it actually tags all remaining rows with a TRUE/FALSE flag, and rate is a proportion, not an average delay in minutes. D inverts the pipeline order entirely — grouping and summarising happen after filtering, not before.
A reliable strategy: when a pipeline mixes filter(), mutate(), and summarise(), mentally "run" each verb in sequence and ask which rows still exist before interpreting what the summary statistics actually measure.A data frame has one row per company and separate revenue columns Q1, Q2, Q3, and Q4. The analyst runs:
companies |> pivot_longer(Q1:Q4, names_to = 'quarter', values_to = 'revenue') |> filter(revenue > 0)
Which narrative most precisely explains the resulting data?
pivot_longer() and filter(), pause and think through each transformation step-by-step before reading the answer choices.
pivot_longer(Q1:Q4, names_to = 'quarter', values_to = 'revenue') reshapes the wide data frame into a long format — each company now appears in four rows, one per quarter, with a quarter column holding "Q1"/"Q2"/etc. and a revenue column holding the corresponding value. Then filter(revenue > 0) keeps only rows where revenue is strictly greater than zero. This means rows with zero revenue, negative revenue, or NA revenue are all dropped — because NA > 0 evaluates to NA, which filter() treats as FALSE. Each surviving row uniquely identifies one company-quarter combination, making A the precise description.
B is wrong on two fronts: the result is still in long format (not wide with separate quarterly columns), and the filter operates row-by-row, not on annual totals. C is wrong because pivot_longer() does not aggregate across companies — each row is still a single company-quarter, not a quarter-level summary — and neither function converts any values to zero. D is tempting but subtly wrong: "nonnegative" would include zero, but the strict inequality > 0 excludes zeros, so they are removed just like negative values.
A good strategy: always trace what a single row represents after each transformation — shape first (pivot_longer changes row meaning), then filter. Misreading the unit of observation is the most common trap in tidy-data questions.To identify required identifiers that have no recorded result, an analyst runs:
expected |> distinct(id) |> anti_join(observed |> distinct(id), by = 'id')
Which statement best documents what this pipeline returns?
distinct() with anti_join(), focus on two things: which table is on the left and what anti_join actually filters. The rule is simple — anti_join(x, y) returns rows from x that have no match in y. It never returns rows from y, and it never produces matches.
Here, the left-hand side is expected |> distinct(id), so the pipeline starts with unique identifiers from the expected table. Then anti_join(..., observed |> distinct(id), ...) filters those down to only the identifiers that don't appear in the observed table. The result is a set of expected IDs with no recorded observation — exactly what D describes: one row for each distinct expected identifier that does not occur among the distinct observed identifiers.
A is wrong because it describes a symmetric operation (rows from either source), which resembles a full join with filtering — not anti_join. B flips the tables entirely: it describes filtering observed IDs against expected ones, which would require observed |> distinct(id) |> anti_join(expected |> distinct(id), ...). C describes a join that retains matches and fills missing fields — that's a left join behavior, not an anti-join, which by definition returns no matched rows.
A reliable study tip: always identify the left table in anti_join(x, y) — that's the pool you're filtering from. The right table is only used to define what gets excluded. This asymmetry is the most commonly tested trap with filtering joins in R.customers contains exactly one row per customer. orders can contain several rows per customer and no rows for customers who have never ordered. An analyst runs:
customer_orders <- left_join(customers, orders, by = 'customer_id')
Which explanation correctly communicates the possible effect of this join on the rows?
left_join(x, y) guarantees every row in the left table (customers) appears in the output — no customer is ever dropped. From there, the row count depends on how many matches exist in the right table (orders). If a customer has three orders, that customer produces three rows in the output. If a customer has zero orders, they still appear once, but all columns sourced from orders fill with NA. This is exactly what A describes, making it the correct answer.
B is tempting but wrong — left_join never automatically collapses multiple matches into a list. That behavior would require a separate aggregation step (e.g., group_by + summarise or nest). C describes an inner_join, which only retains rows with matches in both tables, discarding customers who have never ordered. D describes a right_join or the mistaken idea that the right table drives which rows survive — in a left_join, the right side (orders) never determines whether a left-side row is kept; only the left table does.
A reliable mental model: the word "left" tells you the left table is protected. Every row there survives. The right table only controls how many times a left row repeats (once per match) and whether columns from the right are NA (no match). Keep this rule in mind whenever you encounter any of the four join types.An analyst creates a training and test split using:
set.seed(42)
idx <- sample(seq_len(nrow(df)), size = floor(0.8 * nrow(df)))
train <- df[idx, ]
test <- df[-idx, ]
Which explanation correctly describes both the split and the role of set.seed()?
set.seed() guarantees (and doesn't guarantee).
The code samples 80% of row indices into idx, assigns those rows to train, and uses negative indexing (-idx) to assign the remaining rows to test. These two subsets are mutually exclusive — no row appears in both. This makes D the correct answer: the sampled rows form training data, the complement forms test data, and set.seed(42) ensures you get the same random sample every time you run the code — provided you're using the same R version and RNG algorithm. That last caveat is important.
Here's where the distractors mislead you. A correctly identifies the complement as test data but overclaims about set.seed() — seeds do not guarantee identical results across all R versions and platforms, because R's default RNG algorithm has changed between versions. B contains two errors: it wrongly suggests rows can overlap between subsets (negative indexing makes that impossible), and it misrepresents the seed as controlling "sampling frequencies" rather than reproducibility. C describes a completely different operation — sorting rows before slicing — which is not what sample() does. sample() draws random indices; it doesn't reorder the data frame before taking the first 80%.
As a study habit, always trace index operations carefully in R questions. When you see df[-idx, ], that's R's way of saying "everything except these rows" — a clean complement with zero overlap.The variables x and y are processed with the following pipeline:
df |> mutate(across(c(x, y), ~ replace(.x, is.na(.x), median(.x, na.rm = TRUE)))) |> mutate(across(c(x, y), ~ (.x - mean(.x)) / sd(.x)))
Which explanation correctly describes the order and scope of the transformations?
mutate(across(...)) calls, think about two things: what each lambda does independently per column, and what data that lambda sees when it executes.
In R, across() applies a function to each specified column separately — not across columns simultaneously. So in the first mutate, ~ replace(.x, is.na(.x), median(.x, na.rm = TRUE)) runs on x alone, then on y alone. Each column's NAs are filled with that column's own median, computed from its non-missing values. The result is two fully-imputed columns. In the second mutate, ~ (.x - mean(.x)) / sd(.x) then standardizes each completed column using that column's own post-imputation mean and standard deviation — because .x now refers to the already-imputed version. This confirms C as correct.
A is wrong because it implies a pooled median across both columns. across() never merges columns — each lambda receives one column at a time as .x, so the median is always column-specific.
B reverses the order entirely. Standardization happens in the second mutate, not the first. The pipeline imputes first, then standardizes — not the other way around.
D claims missing values are replaced by each column's mean, but the code explicitly uses median(). It also incorrectly suggests only pre-imputation values drive the standard deviation; after imputation, the filled values participate in sd().
Study tip: When reading pipelines, trace what .x represents at each step — it reflects the column's current state after all prior mutations, not the original raw data.