Did you know: tardigrades are animals about 0.5 mm long, with eight stubby legs. The German pastor
J.A.E. Goeze first described them in 1773 and called them kleiner
Wasserbär: “little water bear”.
Source: National Geographic
A data set with 344 penguins is too much to take in at once. A descriptive statistic summarises it in one number, such as a typical value or how much the values vary.
Like a map, a summary leaves most of the detail out. Today is about choosing a good summary, and noticing when it hides something important.
{dplyr}A distribution shows how often each value, or range of values, occurs.
We compare the two all the time: does our sample look like a shape we know?
{ggplot2}Last week’s promise: {ggplot2} builds a plot in layers,
added with +.
ggplot(data, aes(x = column)) + # the data, and which column goes where
geom_histogram() + # how to draw it
labs(x = "Axis label") # labelsggplot() and aes() say
which data to use and which column goes on x,
y or colourgeom_*() draws the shapes:
geom_histogram(), geom_point(),
geom_boxplot()labs() and theme() change
the labels and the lookOur slides use a theme set up in the hidden first chunk of the .Rmd: your plots will look different, but show the same data.
ggplot(penguins, aes(x = flipper_length_mm)) +
geom_histogram(binwidth = 3, fill = "#5aa0e0", colour = "#0b110e") +
labs(x = "Flipper length (mm)", y = "Count")Each bar counts the penguins whose flipper length falls in a 3 mm bin.
ggplot(penguins, aes(x = flipper_length_mm)) +
geom_histogram(aes(y = after_stat(density)), binwidth = 3,
fill = "#5aa0e0", colour = "#0b110e", alpha = 0.5) +
geom_density(colour = "#f2d45c", linewidth = 1.2) +
labs(x = "Flipper length (mm)", y = "Density")The density curve is a smoothed histogram. The area under it is 1.
In a normal (or Gaussian) distribution, values are spread symmetrically around the mean, and most of them lie close to it: the familiar bell shape.
Two parameters describe it completely: the mean and the standard deviation.
tibble(value = normal) |>
ggplot(aes(x = value)) +
geom_histogram(aes(y = after_stat(density)), bins = 30,
fill = "#e08ab8", colour = "#0b110e", alpha = 0.6) +
stat_function(fun = dnorm, colour = "#f2d45c", linewidth = 1.2) +
labs(x = "Value", y = "Density")Bars: our 500 random values. Line: the theoretical normal curve.
In a normal distribution, about 68% of values lie within 1 SD of the mean, 95% within 2 SD, 99.7% within 3 SD.
Not every variable is normal. Three shapes you will meet in linguistic data:
Accuracy out of a fixed number of trials is binomial (lecture 12). Counts of events, such as pauses per minute, are often Poisson. Reaction times and durations are positive with a long right tail, closer to a log-normal or gamma shape.
Did you know: a protein found only in tardigrades, Dsup,
protects DNA from radiation. Human cells given the gene for it showed
about 40% less DNA damage after X-rays.
Source: Hashimoto et al. (2016), Nature Communications
Descriptive measures come in two kinds:
| Central tendency | Spread |
|---|---|
mean() |
sd(), var() |
median() |
mad(), IQR() |
| mode | range(), quantile() |
Always report one of each: a measure of centre and a measure of spread.
[1] NA
[1] 43.92193
NA means “unknown”. The mean of numbers that include an
unknown is unknown, unless you tell R to drop them.
na.omit() changes your result[1] 43.92193
[1] 43.99279
na.omit() also dropped 9 penguins with a known bill
length but unknown sex.
Rule of thumb: drop NAs only in the columns you are
analysing, with na.rm = TRUE or
drop_na(column).
mean()The mean is the sum of all values divided by their number
mean()# A tibble: 1 × 1
mean_bill
<dbl>
1 43.9
median()The median is the middle value once all values are sorted from lowest to highest: half the values lie below it, half above.
[1] 3
[1] 4
With an even number of values, the median is the mean of the two middle values.
median()penguins |>
summarise(mean_bill = mean(bill_length_mm, na.rm = TRUE),
median_bill = median(bill_length_mm, na.rm = TRUE))# A tibble: 1 × 2
mean_bill median_bill
<dbl> <dbl>
1 43.9 44.4
The overall mean (43.9 mm) sits in a dip: few penguins actually have a bill that long. The distribution has three species in it.
Reaction times cannot be negative, and some responses are very slow:
set.seed(3)
rt <- tibble(rt = rlnorm(2000, meanlog = log(600), sdlog = 0.35))
rt |> summarise(mean = mean(rt), median = median(rt))# A tibble: 1 × 2
mean median
<dbl> <dbl>
1 636. 602.
A long right tail pulls the mean (dashed) away from the median (dotted).
One participant fell asleep on one trial:
set.seed(7)
rts <- round(rlnorm(40, meanlog = log(600), sdlog = 0.25))
rts_sleepy <- c(rts, 5000)
tibble(data = c("40 trials", "40 trials + one 5000 ms"),
mean = c(mean(rts), mean(rts_sleepy)),
median = c(median(rts), median(rts_sleepy)))# A tibble: 2 × 3
data mean median
<chr> <dbl> <dbl>
1 40 trials 665. 626.
2 40 trials + one 5000 ms 770. 628
The mean moves by more than 100 ms. The median barely moves: it is robust.
Here we know that the participant fell asleep. In real data, a 5,000 ms response usually comes with no explanation.
The mode is the most frequent value. It is used in categorical distributions.
With count() from last week:
# A tibble: 1 × 2
island n
<fct> <int>
1 Biscoe 168
R has a function called mode(), but it does not compute
the mode:
[1] "character"
Did you know: when their home dries out, tardigrades
lose their body water, curl up and enter cryptobiosis, a deep sleep. They can revive years
later, when water returns.
Sources: National Geographic; ScienceDaily
Both speakers have a mean of 150 ms. Speaker B is far less consistent.
sd()The standard deviation measures how spread out the values are around the mean: roughly, how far a typical value is from it.
sd()penguins |>
summarise(mean_bill = mean(bill_length_mm, na.rm = TRUE),
sd_bill = sd(bill_length_mm, na.rm = TRUE))# A tibble: 1 × 2
mean_bill sd_bill
<dbl> <dbl>
1 43.9 5.46
mad()The median absolute deviation (MAD) is the robust partner of the SD: the median distance of the values from their median.
| Step | Values | |
|---|---|---|
| 1. The data, sorted | 1, 1, 2, 2, 4, 6, 9 | median = 2 |
| 2. Distance of each value from 2 | 1, 1, 0, 0, 2, 4, 7 | |
| 3. The same distances, sorted | 0, 0, 1, 1, 2, 4, 7 | MAD = 1 |
mad() in R[1] 1
[1] 1.4826
By default, mad() multiplies the result by 1.4826. With
normal data, this makes the MAD estimate the SD, so the two are
comparable.
mad()penguins |>
summarise(median_bill = median(bill_length_mm, na.rm = TRUE),
mad_bill = mad(bill_length_mm, na.rm = TRUE))# A tibble: 1 × 2
median_bill mad_bill
<dbl> <dbl>
1 44.4 7.04
range()range() returns the smallest and the largest value. The
distance between them is what statisticians call the range.
[1] 32.1 59.6
[1] 27.5
The range depends on two values only, the most extreme ones. One outlier changes it completely.
Quantiles cut the sorted values into groups of equal size. Quartiles cut them into four quarters:
0% 25% 50% 75% 100%
32.100 39.225 44.450 48.500 59.600
[1] 9.275
The median is the 50% quantile. The IQR holds the middle half of the data.
summary()Most of today in one line:
Min. 1st Qu. Median Mean 3rd Qu. Max. NA's
32.10 39.23 44.45 43.92 48.50 59.60 2
It works on a whole data frame too: try
summary(penguins).
Box: 25% to 75% (the IQR). Line: the median. Whiskers: to the furthest point within 1.5 × IQR of the box. Dots: every penguin.
group_by and summariseDid you know: in 2007, dried tardigrades flew into orbit
on the FOTON-M3 mission and were exposed to open
space: vacuum, cosmic rays and solar UV over 1,000 times stronger
than on Earth. Most survived the vacuum, a few even the full UV, and the
survivors reproduced normally.
Sources: Jönsson et al. (2008), Current Biology; ScienceDaily
Until now, we have summarised one distribution at a time.
Usually we want the same summary for each group:
bill_length_mm for each species separately.
Last week: group_by() + summarise(), or
summarise(.by = )
penguins |>
drop_na(bill_length_mm) |>
summarise(n = n(),
mean = mean(bill_length_mm),
sd = sd(bill_length_mm),
median = median(bill_length_mm),
IQR = IQR(bill_length_mm),
.by = species)# A tibble: 3 × 6
species n mean sd median IQR
<fct> <int> <dbl> <dbl> <dbl> <dbl>
1 Adelie 151 38.8 2.66 38.8 4
2 Gentoo 123 47.5 3.08 47.3 4.25
3 Chinstrap 68 48.8 3.34 49.6 4.73
One row per species, one column per summary, no copies of the data.
penguins |>
drop_na(sex) |>
group_by(species, sex) |>
summarise(mean_bill = mean(bill_length_mm),
median_bill = median(bill_length_mm))`summarise()` has regrouped the output.
ℹ Summaries were computed grouped by species and sex.
ℹ Output is grouped by species.
ℹ Use `summarise(.groups = "drop_last")` to silence this message.
ℹ Use `summarise(.by = c(species, sex))` for per-operation grouping
(`?dplyr::dplyr_by`) instead.
# A tibble: 6 × 4
# Groups: species [3]
species sex mean_bill median_bill
<fct> <fct> <dbl> <dbl>
1 Adelie female 37.3 37
2 Adelie male 40.4 40.6
3 Chinstrap female 46.6 46.3
4 Chinstrap male 51.1 51.0
# ℹ 2 more rows
The message: the result is still grouped by species. Either add
.groups = "drop", ungroup() it, or use
.by = c(species, sex), which never leaves groups
behind.
group_by() or .by?Use group_by() when several steps in a row should work
per group, and ungroup() when you are done, before you save
the result.
Use .by when only one step needs the groups. Nothing
stays grouped afterwards, so there is nothing to forget.
across()From last week: across(columns, function)
penguins |>
summarise(across(ends_with("_mm"),
list(mean = \(x) mean(x, na.rm = TRUE),
sd = \(x) sd(x, na.rm = TRUE))),
.by = species) |>
select(species, starts_with("bill_length"))# A tibble: 3 × 3
species bill_length_mm_mean bill_length_mm_sd
<fct> <dbl> <dbl>
1 Adelie 38.8 2.66
2 Gentoo 47.5 3.08
3 Chinstrap 48.8 3.34
list(mean = ..., sd = ...) applies two functions to each
column, and adds _mean and _sd to the
names.
bill_summary <- penguins |>
drop_na(bill_length_mm) |>
summarise(mean = mean(bill_length_mm), sd = sd(bill_length_mm), .by = species)
ggplot(drop_na(penguins, bill_length_mm), aes(x = species, y = bill_length_mm)) +
geom_jitter(aes(colour = species), width = 0.2, alpha = 0.3) +
geom_pointrange(data = bill_summary, colour = "#e3e8e1", linewidth = 1, size = 0.8,
aes(y = mean, ymin = mean - sd, ymax = mean + sd)) +
labs(x = NULL, y = "Bill length (mm)") + theme(legend.position = "none")Points: every penguin. White: mean ± 1 SD per species.
In R Markdown, numbers in your text can come straight from R:
becomes
Gentoo bills were 47.5 mm long on average (SD = 3.1).
If the data change, the text updates itself, so you never copy numbers by hand.
mean() (sensitive),
median() (robust), mode with count()sd() (sensitive),
mad() and IQR() (robust),
range(), quantile()summarise() with
.by or group_by(); many columns with
across()These slides
mcanzi.de/ws26_statistics/
Code, data and all lectures
github.com/mcanzi/ws26_statistics