R Programming Quiz: Explaining Analysis Steps
10 questions · exam conditions
0:00
Explaining Analysis StepsQuestion 1 of 10

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?

Sales are standardized using the overall mean and standard deviation, then regions are ordered by their average standardized sales.
Sales are ranked within each region, then each rank is converted to a standard score before the grouping is removed.
Sales are standardized within each region, then all rows are sorted by the resulting scores and returned ungrouped.
Regions are sorted by total sales first, then sales are standardized within the newly established global row order.
← Back to quizzes

R Programming Quiz

R Programming Quiz: Explaining Analysis Steps

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.

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.

How to use this quiz

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.

All questions

Question 1

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?

  1. Sales are standardized using the overall mean and standard deviation, then regions are ordered by their average standardized sales.
  2. Sales are ranked within each region, then each rank is converted to a standard score before the grouping is removed.
  3. Sales are standardized within each region, then all rows are sorted by the resulting scores and returned ungrouped. (correct answer)
  4. Regions are sorted by total sales first, then sales are standardized within the newly established global row order.
Explanation: When you see a dplyr pipeline, read it operation by operation — each verb transforms the result of the previous one, and grouping context determines where calculations happen. Here, 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μσz = \frac{x - \mu}{\sigma}, 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.

Question 2

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?

  1. Scores of at least 90 receive Honors, scores from 80 through 89 receive Pass, and missing scores receive Missing.
  2. Scores of at least 80 receive Pass, including scores of at least 90; missing scores receive Missing, and lower scores receive Review. (correct answer)
  3. Scores from 80 through 89 receive Pass, scores of at least 90 receive both matching labels, and missing scores receive Review.
  4. Scores of at least 80 receive Pass, scores below 80 receive Review, and missing scores remain unlabeled because comparisons return missing values.
Explanation: When using 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.

Question 3

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?

  1. Exactly one employee is returned from each department, with tied employees resolved according to their original row order.
  2. Every employee tied for the highest score in a department is returned, so some departments contribute multiple rows. (correct answer)
  3. Every employee tied for the highest score overall is returned, because ungroup() removes department boundaries before selection.
  4. The two highest-scoring employees per department are returned when first place is tied, because each tie consumes the requested count.
Explanation: When working with 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.

Question 4

An analyst fits lm(log(price) ~ age, data = homes). The estimated coefficient of age is 0.08-0.08, where age is measured in years.

Which statement best explains the coefficient on the original price scale without overstating its meaning?

  1. A one-year age increase is associated with multiplying fitted price by exp(0.08)exp(-0.08), corresponding to about a 7.7%7.7\% decrease. (correct answer)
  2. A one-year age increase causes fitted price to decrease by exactly 8%8\% because the coefficient can be multiplied directly by 100.
  3. A one-year age increase is associated with subtracting 0.080.08 currency units from fitted price on its original measurement scale.
  4. A one-year age increase is associated with multiplying fitted price by exp(0.08)exp(0.08), corresponding to about an 8.3%8.3\% increase.
Explanation: Whenever you fit a log-linear model like 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+β1age\log(\hat{price}) = \beta_0 + \beta_1 \cdot age, so a one-unit increase in age adds β1\beta_1 to the log of price — which means price gets multiplied by eβ1e^{\beta_1} on its original scale. With β^1=0.08\hat{\beta}_1 = -0.08, a one-year age increase multiplies fitted price by e0.080.923e^{-0.08} \approx 0.923, a reduction of roughly 7.7%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 e0.08e^{-0.08}, not 10.081 - 0.08. C confuses the log-scale coefficient with a raw-scale subtraction — subtracting 0.080.08 currency units makes no sense here because the model was built on log(price)\log(price), not price itself. D uses e+0.08e^{+0.08} instead of e0.08e^{-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.

Question 5

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?

  1. Missing delays are treated as not delayed, after which rate and n are calculated from every scheduled flight for each carrier.
  2. Rows with missing delays are removed; rate is the proportion delayed among remaining flights, and n is their count for each carrier. (correct answer)
  3. Rows delayed more than 15 minutes are retained; rate is their average delay, and n is their count for each carrier.
  4. Carrier summaries are computed first, after which carriers with missing delays are removed and their delay rates are recalculated.
Explanation: When reading a dplyr pipeline, trace each step in order — each transformation feeds directly into the next, so the meaning of your final summary depends entirely on what rows survived earlier steps. Here, 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.

Question 6

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?

  1. Each retained row represents a company-quarter with positive observed revenue; zero, negative, and missing revenue entries are all excluded. (correct answer)
  2. Each retained row represents a company with positive total annual revenue; its four quarterly values are kept in separate columns, with zeros set to missing.
  3. Each retained row represents a quarter aggregated across companies; zero and missing entries are converted to a revenue of zero before filtering.
  4. Each retained row represents a company-quarter with nonnegative revenue; only negative and missing values are removed, while zero values remain.
Explanation: When you see a pipeline combining 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.

Question 7

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?

  1. One row for each identifier appearing in exactly one source, regardless of whether it is expected or observed.
  2. One row for each distinct observed identifier that does not occur among the distinct expected identifiers.
  3. One row for every expected-observed match, with missing fields added when an identifier occurs in only one source.
  4. One row for each distinct expected identifier that does not occur among the distinct observed identifiers. (correct answer)
Explanation: When you see a pipeline chaining 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.

Question 8

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?

  1. Every customer remains, customers with multiple orders can produce multiple rows, and customers without orders receive missing order fields. (correct answer)
  2. Every customer remains exactly once, with multiple orders automatically combined into a list and absent orders represented by missing values.
  3. Only customers with at least one order remain, and customers with multiple orders produce one output row for each matching order.
  4. Every order remains exactly once, while customers without orders are discarded because the order data appear on the right side.
Explanation: When you see a join question in R, the key is understanding two things: which table drives the output and how row counts change when the relationship isn't one-to-one. A 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.

Question 9

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()?

  1. The sampled rows form test data and their complements form training data; the seed guarantees that identical samples are produced across all R versions and platforms.
  2. The sampled rows form training data and may also appear in test data; the seed controls sampling frequencies but does not prevent row overlap between subsets.
  3. The first 80 percent of rows form training data after the data are randomly reordered; the seed ensures both subsets are representative of the population.
  4. The sampled rows form training data and the remaining rows form test data; the seed makes the sampling reproducible under the same R environment and RNG settings. (correct answer)
Explanation: When working with train-test splits in R, focus on two things: what the indexing logic actually does and what 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.

Question 10

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?

  1. Missing values in both columns are replaced by one pooled median, then both columns are standardized using a shared mean and standard deviation.
  2. Each column is standardized using only its observed values, then missing standardized values are replaced by the original column's median.
  3. Each column's missing values are replaced by its own observed median, then that completed column is standardized using its post-imputation mean and standard deviation. (correct answer)
  4. Each column's missing values are replaced by its own mean, then the original nonmissing values alone determine the final standard deviation.
Explanation: When you see a pipeline with multiple 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.