Skip to content

Descriptive Statistics & Distributions

Before any test, look at your data. Descriptive statistics summarize a variable in a few numbers. A distribution plot shows its shape. Together they tell you what kind of analysis is even appropriate. This page summarizes gene expression values and read counts, in R and Python. Pick a language with the tabs and the whole page follows.

Every code block on this page runs in a container in the companion repository. The numbers and figures come from those real runs.

Always, and first. Before a t-test, before a model, before a figure for a paper, you summarize each variable and plot its distribution. This is where you catch the problems that break later analysis: skew, outliers, a variable that is counts rather than a continuous measurement, or a batch that does not look like the others. A p-value computed on data you never looked at is a p-value you cannot trust.

We use gene_expression.csv. It has 80 samples, a continuous expression value on the log2 scale, and a discrete counts column of read counts.

df <- read.csv("gene_expression.csv")

The first rows look like this:

sample expression counts
S01 6.37 46
S02 4.75 44
S03 6.90 33
S04 7.13 32
S05 3.66 39

Two numbers describe the center of a variable. The mean is the average. The median is the middle value. Two numbers describe its spread. The standard deviation measures spread around the mean. The interquartile range, or IQR, is the width of the middle half of the data.

summary(df$expression)
mean(df$expression)
median(df$expression)
sd(df$expression)
IQR(df$expression)
Min. 1st Qu. Median Mean 3rd Qu. Max.
3.660 5.447 6.145 6.031 6.753 8.570
mean: 6.031
median: 6.145
sd: 0.929
IQR: 1.305

Both languages agree. The mean is 6.03 and the median is 6.14. They are close, which is a first hint that the distribution is roughly symmetric. The standard deviation is 0.93, and the middle half of the values spans 1.31 log2 units.

When the mean and median are close, either summarizes the center well. When they drift apart, the distribution is skewed, and the median is the more honest summary. Income is the classic example. A few very large values pull the mean up while the median stays put. In bioinformatics the same happens with raw expression and with library sizes. This is one reason expression is usually analyzed on the log scale, where the distribution is more symmetric.

A histogram bins the values and counts how many fall in each bin. A density curve is a smoothed version of the same shape. Together they show the center, the spread, and whether the distribution is symmetric or has a tail.

library(ggplot2)
ggplot(df, aes(x = expression)) +
geom_histogram(aes(y = after_stat(density)), bins = 15,
fill = "#69b3a2", color = "white") +
geom_density(color = "#1f4e5f", linewidth = 1) +
labs(x = "Expression (log2)", y = "Density")

Histogram and density of expression, drawn in R with ggplot2

The expression values form a single rounded peak near 6, roughly symmetric, with no second mode and no extreme outliers. This is the shape a t-test or a linear model expects.

Many tests assume a variable is roughly normal. The clearest visual check is the normal quantile-quantile, or Q-Q, plot. It plots the sorted data against the values a normal distribution would produce. If the points fall on the diagonal line, the data is close to normal. Systematic curves away from the line signal skew or heavy tails.

ggplot(df, aes(sample = expression)) +
stat_qq() +
stat_qq_line()

Normal Q-Q plot of expression in R

The points sit close to the line across the whole range, with only mild wandering at the ends. Expression is close enough to normal for the usual parametric tests. The next guide, Checking assumptions, turns this visual check into a formal test.

Not every variable is continuous. Read counts are whole numbers, they cannot be negative, and their spread grows with their mean. They follow a Poisson or negative binomial distribution, not a normal one. Summarizing counts with a mean and standard deviation, or feeding them straight into a t-test, is a common mistake.

ggplot(df, aes(x = counts)) +
geom_histogram(binwidth = 5, fill = "#e07a5f", color = "white") +
labs(x = "Read counts", y = "Frequency")

Histogram of read counts in R

The counts are discrete and lean to one side. Tools like DESeq2 and edgeR model counts with a negative binomial distribution for exactly this reason. When you meet count data, reach for a count model, not a t-test.

  • Report spread, not just center. A mean with no standard deviation or IQR hides how variable the data is.
  • A skewed variable needs the median. When the mean and median disagree, trust the median and consider a log transform.
  • Counts are not continuous. Read counts, UMI counts, and peak counts follow count distributions. Summarize and model them as counts.
  • Always plot before you test. A histogram takes seconds and catches problems that summary numbers hide, such as two hidden subpopulations in one variable.
  • The mean and median describe the center. The standard deviation and IQR describe the spread.
  • When the mean and median are close, the distribution is roughly symmetric. When they diverge, it is skewed.
  • A histogram with a density curve shows the shape. A Q-Q plot checks for normality.
  • Count data follows a Poisson or negative binomial distribution and needs count-aware methods.
  • STHDA, descriptive statistics and distribution plots.
  • An Introduction to Statistical Learning, chapter 2, for the wider picture on summarizing data.
  • The next guide, Checking assumptions, makes the normality check formal.