Checkpoint: Data Analysis

Reviewed & published by Brayan K

This checkpoint consolidates the data-analysis half of the course — tibbles, reshaping, purrr, lubridate, ggplot2, distributions, hypothesis testing, and glm — into one end-to-end tidyverse pipeline and a self-check quiz.

Part of the free R course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.

You'll recap each tool, tackle a build challenge that flows raw data through pivoting, grouped summaries, and a described plot, then test your understanding with a short checkpoint quiz before moving on to linear models.

What This Checkpoint Covers

📚 What You've Learned

Before the challenge, here's the toolkit you've built across the data-analysis lessons — each row is a job and the verb that does it.

LessonKey tools
Tibblestibble(), as_tibble(), glimpse()
Reshapingpivot_longer(), pivot_wider()
purrrmap(), map_dbl(), reduce(), keep()
lubridateymd(), wday(), floor_date()
ggplot2facet_wrap(), theme_minimal(), labs()
Distributionsdnorm/pnorm/qnorm/rnorm, sample()
Hypothesis testst.test(), wilcox.test(), chisq.test()
glmglm(family = binomial), predict()

🧩 Warm-Up: Reshape, Date, Map

Before the full pipeline, here are three of the module's idioms side by side on inputs small enough to check by eye. Read each comment, then match what prints to what the comment describes.

# Three idioms this checkpoint leans on, each shown on a tiny input.
library(tidyverse)
library(lubridate)

# 1) pivot_longer: many WIDE columns -> two LONG columns (names + values)
wide <- tibble(id = 1:2, jan = c(5, 7), feb = c(6, 9))
long <- pivot_longer(wide, cols = -id, names_to = "month", values_to = "sales")
print(long)     # 4 rows (2 ids x 2 months) with columns id, month, sales

# 2) lubridate: parse text into real Date objects, then ask them questions
d <- ymd(c("2026-01-15", "2026-02-15"))
print(month(d, label = TRUE))   # the month name of each date
print(wday(d, label = TRUE))    # the weekday name of each date

# 3) purrr: apply one function to every element of a list, get a numeric vector back
print(map_dbl(list(a = 1:3, b = 4:6), mean))   # named vector: one mean per element

Your turn. The same three idioms, now chained on one small dataset: reshape it long, parse the dates, and summarise per page with map_dbl(). Replace each ___ with the value in its hint, then run it and compare with the output panel.

🛠️ Build Challenge: A Tidyverse Pipeline

Put it together. Start from a wide inline tibble, reshape it long, summarise per group, and describe the plot you'd draw. Work through the ___s yourself before revealing the solution.

# 🎯 BUILD CHALLENGE — a full tidyverse data-analysis pipeline.
#
# You're given quarterly revenue for three stores in WIDE form. Your
# job: tidy it, summarise it, and describe a plot — exercising tibbles,
# pivoting, dplyr, and ggplot2 in one flow.
library(tidyverse)   # loads tibble, tidyr, dplyr, ggplot2, purrr...

# 1) The raw WIDE data as an inline tibble:
revenue <- tibble(
  store = c("North", "South", "East"),
  Q1    = c(120, 90, 150),
  Q2    = c(135, 110, 140),
  Q3    = c(150, 130, 160),
  Q4    = c(170, 125, 180)
)

# 2) TODO: pivot_longer() the four quarter columns into 'quarter' and
#    'revenue'. Hint: cols = -store.

# 3) TODO: with dplyr, group_by(store) and summarise() the TOTAL and
#    MEAN revenue per store; arrange() by total descending.

# 4) TODO (describe in a comment): what ggplot would you draw to show
#    each store's revenue trend across the four quarters? Name the geom,
#    the x/y aesthetics, and the colour aesthetic.

# Try it yourself first — the full solution is below.
library(tidyverse)

revenue <- tibble(
  store = c("North", "South", "East"),
  Q1    = c(120, 90, 150),
  Q2    = c(135, 110, 140),
  Q3    = c(150, 130, 160),
  Q4    = c(170, 125, 180)
)

# 2) Wide -> long
long <- revenue |>
  pivot_longer(
    cols      = -store,
    names_to  = "quarter",
    values_to = "revenue"
  )

# 3) Per-store summary, sorted by total
summary_tbl <- long |>
  group_by(store) |>
  summarise(
    total = sum(revenue),
    mean  = mean(revenue),
    .groups = "drop"
  ) |>
  arrange(desc(total))

summary_tbl

# 4) The plot: a line chart of revenue across quarters, one coloured
#    line per store, so the trends are directly comparable.
ggplot(long, aes(x = quarter, y = revenue,
                 colour = store, group = store)) +
  geom_line(linewidth = 1) +
  geom_point(size = 2) +
  labs(
    title = "Quarterly revenue by store",
    x = "Quarter", y = "Revenue ($k)", colour = "Store"
  ) +
  theme_minimal()

Mini-Challenge: Extend the Pipeline

Stretch the build challenge further to weave in more of the course:

Build it in RStudio and inspect each intermediate result.

📝 Checkpoint Quiz

Answer each in your head (or in the console), then click to reveal. If any feel shaky, revisit that lesson before continuing.

pivot_longer(). The old column HEADERS go into the new column named by names_to (e.g. "year"), and the CELL values go into the column named by values_to (e.g. "revenue"). The reverse is pivot_wider() with names_from/values_from.

map() always returns a LIST, one element per input. map_dbl() returns a numeric VECTOR and errors if any result isn't a single double — a built-in type check. Use the typed variant whenever you expect a simple atomic result, because it catches mistakes immediately.

p answers "fraction below" — pnorm(60, mean, sd) gives the cumulative probability up to 60. q is its inverse and gives a value at a percentile — qnorm(0.90, mean, sd) returns the 90th-percentile value. d is the density (curve height) and r draws random numbers.

chisq.test() on a contingency table of counts. A t-test compares MEANS of a numeric variable; here the data are COUNTS of a categorical outcome, so chi-squared (testing association between layout and sign-up) is the right tool. For small, sparse tables, fisher.test() is even better.

Coefficients are log-odds, so apply exp(coef(model)) to read them as ODDS RATIOS (above 1 raises the odds, below 1 lowers them). For a probability on a new case, use predict(model, newdata, type = "response") — without type = "response" you'd get log-odds instead of a [0, 1] probability.

Both expect tidy data — one variable per column. If a variable (like "quarter") is spread across column headers, you can't map it to an aesthetic or group by it. Pivoting it into a real column makes it addressable, so aes(x = quarter) and group_by(quarter) work.

Pro Tips for Real Analyses

🎉 Checkpoint Complete!

Practice quiz

Which function turns a WIDE table (one column per quarter) into LONG form?

  • pivot_wider()
  • spread()
  • pivot_longer()
  • gather_cols()

Answer: pivot_longer(). pivot_longer() collapses many columns into two: a names column and a values column. pivot_wider() does the reverse.

In pivot_longer(cols = -store, names_to = 'quarter', values_to = 'revenue'), where do the OLD column headers (Q1, Q2...) end up?

  • In the 'quarter' column
  • In the 'revenue' column
  • They are dropped
  • In the row names

Answer: In the 'quarter' column. names_to names the column that receives the old headers; values_to names the column that receives the cell values.

What does purrr's map() always return?

  • A numeric vector
  • A data frame
  • A single scalar
  • A list, one element per input

Answer: A list, one element per input. map() always returns a list. map_dbl() returns a double vector and errors if any result is not a single double.

Why use map_dbl() instead of map() when you expect numbers?

  • It is the only one that works on vectors
  • It returns a numeric vector and type-checks each result
  • It runs in parallel
  • It sorts the output

Answer: It returns a numeric vector and type-checks each result. map_dbl() returns an atomic double vector and errors immediately if a result is not a single double, catching mistakes early.

In the d/p/q/r distribution system, which prefix answers 'what fraction of values fall below 60?'

  • p (cumulative probability)
  • d (density)
  • q (quantile)
  • r (random)

Answer: p (cumulative probability). pnorm(60, mean, sd) gives the cumulative probability up to 60. q is its inverse (value at a percentile), d is density, r draws random numbers.

Which d/p/q/r prefix returns the 90th-percentile VALUE of a distribution?

  • pnorm
  • dnorm
  • qnorm
  • rnorm

Answer: qnorm. qnorm(0.90, mean, sd) returns the value at the 90th percentile. It is the inverse of pnorm.

Your outcome is 'signed up: yes/no' counted across two page layouts. Which test fits?

  • t.test()
  • chisq.test()
  • wilcox.test()
  • rnorm()

Answer: chisq.test(). The data are counts of a categorical outcome, so chisq.test() on a contingency table tests association. A t-test compares numeric means.

After glm(y ~ x, family = binomial), how do you turn a coefficient into an odds ratio?

  • log(coef(model))
  • 1 / coef(model)
  • coef(model)^2
  • exp(coef(model))

Answer: exp(coef(model)). glm coefficients are log-odds, so exp() converts them to odds ratios (above 1 raises the odds, below 1 lowers them).

To get a predicted PROBABILITY (in [0,1]) from a binomial glm, which call do you use?

  • predict(model, newdata)
  • predict(model, newdata, type = 'response')
  • summary(model)
  • coef(model)

Answer: predict(model, newdata, type = 'response'). Without type = 'response' predict() returns log-odds; type = 'response' maps them to a probability between 0 and 1.

Why pivot to LONG form before calling ggplot() or group_by()?

  • Long form uses less memory
  • Wide form cannot be plotted at all
  • They expect tidy data with one variable per column
  • It is required for fread()

Answer: They expect tidy data with one variable per column. Both need tidy data. A variable trapped in column headers (like 'quarter') must become a real column before you can map it to an aesthetic or group by it.

Continue this course

Frequently asked questions

I can do each step alone but freeze on the full pipeline. Help?

That's the normal stage right after learning the pieces — integration is its own skill. Build the pipeline one verb at a time and PRINT after every step: start with the raw tibble, add pivot_longer() and look, then pipe into group_by()/summarise() and look again. Because the |> pipe passes the left side into the next function, you can comment out the tail and inspect the head whenever something surprises you. Once each intermediate result matches your expectation, the whole chain is just those verified steps stitched together.

When do I pivot longer versus keep data wide?

Pivot LONGER to compute and plot: dplyr's group_by()/summarise() and ggplot2's aesthetics both need every variable to be its own column, so a quarter trapped in column headers must become a real 'quarter' column first. Keep or pivot WIDER to PRESENT: a final report table is easier for humans to read with quarters across the top. A typical analysis spends its middle in long form and only widens the very last summary for display.

Should I reach for purrr or a dplyr summarise here?

For per-group summaries of a single tidy data frame, dplyr's group_by() |> summarise() is clearer and faster to write — that's its home turf. Reach for purrr's map() family when you're iterating over a LIST of things: many files to read, many groups to fit a separate model to (nest then map), or applying a function across list columns. A common advanced pattern combines both: group and nest with dplyr/tidyr, then map() a model over each nested tibble.

How do I decide between t.test, chisq.test, and glm at the end of an analysis?

Match the tool to the question and the data type. Comparing the MEAN of a numeric variable across one or two groups -> t.test (or wilcox.test if normality is doubtful). Testing whether two CATEGORICAL variables are associated using counts -> chisq.test. MODELLING an outcome from one or more predictors, especially a binary yes/no outcome -> glm with family = binomial. If you only need a single yes/no comparison, a test suffices; if you need to predict or adjust for several predictors at once, fit a model.