3 Chapter 3 — Describing What You See
Applied Epidemiology in Aquaculture — Part I: Foundations
Where this sits — Foundations. Describing data honestly is the common groundwork for measuring, comparing, and acting across all five phases of the framework.
3.1 Overview
Before you can test whether river water causes more BKD, or whether a vaccine works, you have to be able to describe what is in front of you — clearly, honestly, and in numbers other people can check. This is descriptive epidemiology, and it is where every investigation begins: summarising disease and its determinants by animal, place, and time (Thrusfield & Christley, 2018).
This chapter teaches the descriptive toolkit a fish health specialist actually uses: how to recognise the type of each variable, how to summarise its centre and its spread, and how to see its shape with a first set of plots. We keep using the aquafarms_register.csv dataset from Chapter 2, and — as you will see — even this small register hides two traps that catch people every week. By the end you will know which summary to trust, and when the “average” is quietly lying to you.
3.2 3.1 Preparation
Open your project and start a new script. Load the spine and import the register, exactly as in Chapter 2:
pacman::p_load(rio, here, tidyverse, janitor, gtsummary)
register <- import(here("data", "aquafarms_register.csv"))We will refer often to prevalence — the share of sampled animals that tested positive — so let’s compute it once and keep it as a new column with mutate():
register <- register |>
mutate(prevalence = round(n_positive / n_sampled * 100, 1))3.3 3.2 Know your variables
Every column has a type, and the type decides which summary and which plot are legal. Mixing this up is the single most common beginner mistake. Two big families:
Quantitative (numeric) variables measure how much: - Continuous — can take any value on a scale: water_temp_c, mean_weight_g, prevalence. - Discrete — whole-number counts: n_stocked, n_positive, dead_this_month.
Qualitative (categorical) variables describe which kind: - Nominal — unordered categories: species, farm, system. - Ordinal — ordered categories (none here yet, but e.g. a lesion score “mild < moderate < severe”). - Binary — a special two-level case: “SRS-positive: yes/no”.
Why this matters. You can take the mean of a continuous variable (mean temperature), but the “mean” of
speciesis nonsense — there you count categories instead. Get the type right and the correct summary almost chooses itself.
3.4 3.3 The centre of the data
Three classic measures of central tendency, plus one that fish health work needs more than most fields.
- Mean — the arithmetic average;
mean(). - Median — the middle value when sorted;
median(). Half the data lie below it. - Mode — the most frequent value; most useful for categories (which species is commonest).
Start with the eight salmon water temperatures, a clean, well-behaved variable:
salmon_temps <- register |>
filter(species == "Atlantic salmon") |>
pull(water_temp_c)
round(mean(salmon_temps), 2) # 12.29
median(salmon_temps) # 12.2Mean and median agree (≈12.3 °C), which tells you the salmon temperatures are roughly symmetric. Good. Now watch what happens when we are careless and average across all the farms.
3.4.1 Trap 1 — the average that describes nobody
round(mean(register$water_temp_c), 1) # 23.1
median(register$water_temp_c) # 27.35The “average farm temperature” is 23.1 °C — but not a single unit in the dataset is anywhere near 23 °C. Our farms live in two separate thermal worlds (salmon near 12 °C; tilapia and shrimp near 28–30 °C), so the overall mean lands in the empty gap between them. This is a bimodal distribution, and any single summary number is misleading. The fix is not a cleverer statistic; it is to describe by group (we do this in §3.6).
3.4.2 Trap 2 — the mean that gets hijacked
round(mean(register$n_stocked)) # 86882
median(register$n_stocked) # 30489.5Here the mean is nearly three times the median. The reason is the shrimp ponds: with hundreds of thousands of animals each, they drag the mean far above where most units sit. This is right-skew, and it is everywhere in aquaculture data — stocking numbers, sea-lice counts, bacterial loads. When data are skewed, the median is the honest summary of the typical unit, and the mean describes the pull of the extremes. Reporting a mean sea-lice count without saying the data are skewed can seriously overstate a “typical” infestation (Lepe-López et al., 2021).
The geometric mean deserves a mention because fish health lives on multiplicative scales. Pathogen loads and antibody titres are often reported as a geometric mean — the back-transformed mean of the logged values — because a jump from 10 to 100 copies matters like a jump from 100 to 1,000. Compute it as
exp(mean(log(x))). We return to it when we handle qPCR data.
3.5 3.4 The spread of the data
The centre is only half the story; two farms can share a mean and behave completely differently. Measures of dispersion quantify how much the values vary.
- Range — minimum to maximum;
range(). Simple, but driven entirely by the two most extreme points. - Quantiles and the IQR — the 25th and 75th percentiles bracket the middle half of the data; their difference is the interquartile range,
IQR(). Robust to outliers, so it pairs naturally with the median. - Variance and standard deviation — the average squared distance from the mean, and its square root;
var()andsd(). The SD is in the same units as the data, so it is the one you report. - Coefficient of variation (CV) — the SD as a percentage of the mean (
sd(x)/mean(x)*100). Because it is unit-free, it lets you compare variability across things measured on different scales.
round(sd(salmon_temps), 2) # 0.98 -> salmon cages are thermally very similar
IQR(register$prevalence) # spread of the middle 50% of prevalencesThe CV is the tool for a question like “which varies more between units — salmon weight or shrimp weight?”, where the raw SDs are not comparable because a salmon weighs ~2,900 g and a shrimp ~18 g.
3.6 3.5 The shape of the data
Numbers summarise; pictures reveal. A histogram shows the distribution of one continuous variable, and it is the fastest way to catch skew, bimodality, and errors. This is our first use of ggplot2 (part of the tidyverse you already loaded). Its logic: name the data, map a variable to an axis with aes(), then add a geom layer with +.
ggplot(register, aes(x = n_stocked)) +
geom_histogram(bins = 10) +
labs(title = "Stocking numbers are right-skewed",
x = "Animals stocked per unit", y = "Number of units")You will see the long right tail created by the shrimp ponds — the picture of Trap 2.
To see Trap 1, compare the thermal worlds directly with a boxplot, which draws the median, the IQR (the box), and outliers for each group:
ggplot(register, aes(x = species, y = water_temp_c)) +
geom_boxplot() +
labs(x = NULL, y = "Water temperature (°C)")The two separated clusters make the bimodality obvious — and explain why a single overall mean was meaningless. The same plot for prevalence by species is your first visual comparison of disease burden across the three farms.
Why shape matters beyond description. Many statistical tests (Chapter 5 onward) assume roughly symmetric, “normal” data. When a variable is strongly skewed — as counts usually are — you either use methods built for that shape or transform the variable (often with a log) first. Looking at the histogram now tells you which path you are on later.
3.7 3.6 Describing like an epidemiologist
Pulling it together, the descriptive habit is to summarise by group — the “place/animal” axis — rather than lumping everything. dplyr does this cleanly:
register |>
group_by(species) |>
summarise(
n_units = n(),
mean_temp = mean(water_temp_c),
med_stocked = median(n_stocked),
mean_prev = mean(prevalence)
)This single table respects both traps: temperature and stocking are reported within each species, where they are meaningful. For a publication- or report-ready version, gtsummary::tbl_summary() builds a formatted descriptive table in one call:
register |>
select(species, water_temp_c, mean_weight_g, prevalence) |>
tbl_summary(by = species)The remaining axis, time, is flat here because the register is a single month. Describing disease over time — the epidemic curve — is important enough to get its own treatment later (Chapter 13); the retrospective description of weekly SRS mortality by season in Diethelm-Varela et al. (2025) is a good example of that descriptive step done well before any modelling.
3.8 3.7 Interpretation on the farm
Descriptive statistics are not a warm-up you rush through to reach the “real” analysis — on a farm they are often the analysis. The grouped table above already tells a manager where to look: which species carries the highest average prevalence this month, how tightly the cages track each other thermally, and whether any unit is an outlier worth a site visit. And the two traps are not academic: a consultant who reports a single mean across mixed systems (Trap 1), or a mean sea-lice count on skewed data (Trap 2), will mislead a client with a number that is technically correct and practically useless. The discipline of checking the variable type, then the shape, then choosing the summary, is what separates a defensible report from a dangerous one.
3.9 3.8 Exercises
Try it yourself (3.1). Build the grouped summary in §3.6, then add a column for the standard deviation of
mean_weight_g. Which species’ units vary most in weight?
3.2 Compute the mean and median of prevalence for the whole register. Are they close? What does that tell you about the shape of the prevalence distribution, without plotting it?
3.3 Make a histogram of mean_weight_g. Why is it not a smooth single peak? (Think about what you learned in Trap 1.)
3.4 Calculate the coefficient of variation of water_temp_c separately for salmon and for tilapia. Which species experiences more relative thermal variability between its units?
3.5 A colleague reports “average stocking across the operation is about 87,000 animals per unit.” Explain in one or two sentences why this is misleading, and propose a better one-sentence description.
3.10 Summary & what’s next
- Descriptive epidemiology summarises disease by animal, place, and time, and it is where every investigation starts.
- Each variable has a type (quantitative continuous/discrete; qualitative nominal/ordinal/binary) that dictates its summary and plot.
- Centre: mean, median, mode — and the geometric mean for loads and titres. The median is the honest summary when data are skewed.
- Spread: range, IQR, variance, SD, and the unit-free CV for comparing across scales.
- Shape: histograms and boxplots reveal skew and bimodality that summaries hide — our data showed a bimodal temperature (the mean describes nobody) and right-skewed stocking (the mean is hijacked by shrimp ponds).
- Always describe by group;
dplyr::summarise()andgtsummary::tbl_summary()are your workhorses.
In Chapter 4 — How Much Disease Is There? we convert the raw counts in this register into the two foundational measures of disease frequency: prevalence and incidence. The prevalence column you created here is the first step.
3.11 References & further reading
- Thrusfield M, Christley R. Veterinary Epidemiology. 4th ed. Wiley-Blackwell; 2018. — foundational text for describing data and the animal/place/time framework.
- Lepe-López M, Escobar-Dodero J, Rubio D, Alvarez J, Zimin-Veselkoff N, Mardones FO. Epidemiological factors associated with Caligus rogercresseyi infection, abundance, and spatial distribution in Southern Chile. Frontiers in Veterinary Science. 2021;8:595024. — a real example of describing highly skewed count data across hundreds of farms.
- Diethelm-Varela B, Atero N, Córdova-Bührle F, Rezende EL, Gelcich S, Sandoval O, Navarro C, Mardones FO. Epidemiology of salmonid rickettsial septicemia (SRS) in farmed salmon: the role of sea lice infestations in mortality risk. Journal of Fish Diseases. 2025. — descriptive summary of weekly mortality by season preceding the analysis.
- Batra N, et al. The Epidemiologist R Handbook. Applied Epi; 2021. (epirhandbook.com) — practical R recipes for descriptive tables and
ggplot2.
Files for this chapter: reuses data/aquafarms_register.csv from Chapter 2 (no new dataset).