Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

L07 — Exploratory Data Analysis: favstats, tally, gf_ Plots, Faceting

R Help for Beginners

1Objectives

By the end of this lesson you will be able to:

  1. Summarize a numerical variable, overall and by group, with favstats().

  2. Build one-way and two-way frequency and proportion tables with tally().

  3. Build a histogram, boxplot, bar chart, and scatterplot with the gf_* (ggformula) family.

  4. Facet a plot into small multiples with gf_facet_wrap().

  5. Apply the Okabe–Ito colorblind-safe palette to any plot with a color/fill grouping.

2EDA: look before you test

Exploratory data analysis (EDA) means looking at your data — numbers and pictures — before you run any formal test. Every inference method later in this guide (starting L10) has conditions to check, and you cannot check a condition like “roughly symmetric” or “no extreme outliers” without first looking. favstats(), tally(), and the gf_* plotting family are your looking tools. This lesson uses the running survey_sim dataset throughout — reload it if you’re starting a fresh session:

suppressMessages({library(mosaic); library(BSDA)})
survey <- read.csv("data/survey_sim.csv")

3Numerical summaries with favstats(), by group

L05 introduced favstats(). Here it is again, now grouped by a categorical variable — the formula pattern you’ll use constantly:

favstats(sleep_hours ~ pet, data = survey)
         pet min    Q1 median   Q3 max     mean        sd  n missing
1 Cat person 4.2 6.075   6.65 7.20 9.1 6.687500 1.0268667 40       0
2 Dog person 3.9 6.050   6.80 7.55 8.8 6.763636 1.0660352 55       0
3    Neither 5.3 6.600   7.00 7.50 8.6 7.000000 0.8631338 25       0

Read this the same way as any favstats() table: one row per group (40 cat people, 55 dog people, 25 neither — the group sizes are right there in the n column), each with its own five-number summary, mean, and SD. Reading across, the three groups’ medians (6.65, 6.80, 7.00 hours) are close — pet preference does not look strongly related to sleep in this sample. That kind of read-back, in one sentence, is exactly what an EDA step should produce.

4Frequency and proportion tables with tally()

A one-way tally counts one categorical variable:

tally(~ major_area, data = survey)
major_area
   Business Kinesiology     Nursing       Other        STEM 
         31          24          19          22          24 

31 of 120 students in this sample listed Business as their major area, and so on — a frequency count, one number per category.

A two-way tally cross-classifies two categorical variables into a contingency table — L11’s chi-square tests are built directly on tables shaped exactly like this one:

tally(pet ~ class_year, data = survey)
            class_year
pet          Freshman Sophomore Junior Senior
  Cat person        9        14     11      6
  Dog person       16        11     16     12
  Neither           5        11      6      3

Each cell is a count of students with that exact combination — e.g., 9 freshmen are cat people. Raw counts alone can be hard to compare across columns of different sizes, so add format = "proportion" to convert each column into proportions that sum to 1:

tally(pet ~ class_year, data = survey, format = "proportion")
            class_year
pet           Freshman Sophomore    Junior    Senior
  Cat person 0.3000000 0.3888889 0.3333333 0.2857143
  Dog person 0.5333333 0.3055556 0.4848485 0.5714286
  Neither    0.1666667 0.3055556 0.1818182 0.1428571

Now you can read straight across a row meaningfully: roughly 29–39% of students in every class year are cat people — a fairly stable proportion across years, which is the kind of pattern raw counts alone can hide.

5Plots: the gf_* (ggformula) family

Every gf_* function follows the identical formula pattern as favstats() and tally()gf_type( y ~ x, data = ), read the same “y broken down by x” way. This section builds one plot type at a time, all from survey.

5.1Histogram: one numerical variable

gf_histogram(~ sleep_hours, data = survey, binwidth = 0.5,
             fill = "#0072B2", color = "white") %>%
  gf_labs(title = "Sleep hours, first-day campus survey (n = 120)",
          x = "Hours of sleep last night", y = "Number of students")
Histogram of 120 students' sleep hours, roughly bell-shaped and centered near 7 hours, with a tall bar around 7 hours and a few students reporting as little as 4 or as much as 9 hours.

Figure 1:Histogram of sleep hours from the first-day campus survey.

binwidth = 0.5 sets each bar to cover half an hour; fill sets the bar color (here, one of the Okabe–Ito colorblind-safe colors used throughout this book — see the callout below); color = "white" draws a thin border between bars so adjacent bars stay visually separate. %>% (the pipe) means “take what’s on the left, feed it into the next function” — it lets you build a plot in readable steps instead of one long nested call. Reading the picture: the distribution is roughly bell-shaped, centered a little below 7 hours, with a noticeable dip around 5–5.5 hours and a tall spike right at 7 — worth a mental note, though with only 120 students some bumps are just sampling noise.

5.2Boxplot: one numerical variable, by group

gf_boxplot(sleep_hours ~ pet, data = survey, fill = ~pet) %>%
  gf_refine(scale_fill_manual(values = c("#E69F00", "#0072B2", "#009E73"))) %>%
  gf_labs(title = "Sleep hours by pet preference",
          x = "Pet preference", y = "Hours of sleep last night",
          fill = "Pet preference")
Side-by-side boxplots of sleep hours for cat people, dog people, and students who are neither, showing similar medians around 6.7 to 7 hours across all three groups, with two outlier points for cat people.

Figure 2:Boxplots of sleep hours by pet preference, matching the favstats() table above.

This is the same sleep_hours ~ pet formula you used with favstats() above, now as a picture — and it shows the same story: three boxes with close, overlapping medians. fill = ~pet colors each box by group; scale_fill_manual(values = c(...)) inside gf_refine() forces those colors to specific Okabe–Ito hex codes rather than ggplot2’s (not colorblind-safe) defaults. Note the plot also labels each group on the x-axis — color is a helpful visual grouping cue here, never the only way to tell the groups apart, which is what accessibility standards require.

5.3Bar chart: one categorical variable

okabe_ito <- c("#E69F00", "#56B4E9", "#009E73", "#F0E442",
               "#0072B2", "#D55E00", "#CC79A7", "#000000")
gf_bar(~ major_area, data = survey, fill = ~major_area) %>%
  gf_refine(scale_fill_manual(values = okabe_ito)) %>%
  gf_labs(title = "Declared major area, first-day campus survey",
          x = "Major area", y = "Number of students")
Bar chart of five declared major areas among 120 surveyed students. Business has the most students at 31, followed by Kinesiology, STEM, and Other in the low-to-mid twenties, and Nursing the fewest at 19.

Figure 3:Bar chart of major area, matching the tally(~ major_area) counts above.

gf_bar() counts rows per category automatically — it’s the picture version of the tally(~ major_area) table above; you can check the bar heights against those exact counts (31, 24, 19, 22, 24). Each category also gets its own Okabe–Ito color and, more importantly, its own labeled position on the x-axis, so the category identity never depends on color perception alone.

5.4Scatterplot: two numerical variables, with a fitted line

gf_point(exam_score ~ study_min, data = survey, color = "#0072B2", alpha = 0.75) %>%
  gf_lm(color = "#D55E00") %>%
  gf_labs(title = "Exam score vs. weekly study time",
          x = "Weekly study time (minutes)", y = "Exam 1 score (points)")
Scatterplot of exam score against weekly study minutes for 120 students, showing a clear positive, moderately scattered relationship, with an upward-sloping trend line from about 48 to 76 points as study time increases from 0 to 500 minutes.

Figure 4:Scatterplot of exam score vs. weekly study time, with a fitted line from gf_lm().

gf_point(y ~ x, data = ) puts study_min on the x-axis and exam_score on the y-axis; gf_lm() (piped right after) adds a fitted straight line — L12 covers fitting and interpreting that line in full. The numerical version of “how strong is this pattern” is the correlation coefficient:

cor(exam_score ~ study_min, data = survey)
[1] 0.5594575

A correlation of about 0.56 matches what the picture shows: a real, positive, but not perfectly tight relationship — more study time tends to go with a higher score, with plenty of individual scatter around the trend.

6Faceting: one plot per group, side by side

Faceting splits a single plot into a grid of small panels, one per level of a grouping variable — useful when a color-by-group plot would get too crowded, or when you want each group’s shape to be easy to compare on its own axis:

gf_histogram(~ sleep_hours, data = survey, binwidth = 0.5,
             fill = "#009E73", color = "white") %>%
  gf_facet_wrap(~ class_year) %>%
  gf_labs(title = "Sleep hours by class year",
          x = "Hours of sleep last night", y = "Number of students")
Four histograms of sleep hours, one panel per class year (Freshman, Sophomore, Junior, Senior), each roughly centered between 6 and 7.5 hours; the Sophomore panel has a noticeably tall spike of about 15 students right at 7 hours.

Figure 5:Sleep hours faceted by class year — one histogram panel per group.

gf_facet_wrap(~ class_year) is simply piped onto the same histogram code from the very first example — faceting is an add-on step, not a different plot type. Each panel keeps the same x-axis (hours of sleep) so the four groups’ shapes are directly comparable at a glance; the labeled panel strips (Freshman, Sophomore, ...) identify each group by text, not color, which is why faceting is often the most accessible way to compare more than two or three groups.

7Choosing a plot: a quick guide

QuestionPlotFunction
What’s the shape of one numerical variable?Histogramgf_histogram(~x, data=)
How does one numerical variable compare across groups?Boxplotgf_boxplot(y ~ g, data=)
How many cases fall in each category?Bar chartgf_bar(~x, data=)
How are two numerical variables related?Scatterplotgf_point(y ~ x, data=)
Compare a plot’s shape across many groups at once?Faceted version of any of the above%>% gf_facet_wrap(~g)

8Summary