Descriptive Statistics, From Mean to Quartiles
A grounded explainer on descriptive statistics (mean, median, mode, variance, standard deviation, range, quartiles, IQR), pinning down the population-vs-sample variance choice and the non-interpolating quartile method that loopaloo's Statistics Calculator and CSV Statistics Generator actually implement.
Descriptive statistics is the part of statistics that does not try to predict anything. It just tells you what a pile of numbers looks like: where the middle is, how spread out the values are, and whether anything is sitting way out in the tail. It is the first thing you run on a new dataset, and it is the layer most people get subtly wrong, usually because two reasonable definitions disagree and nobody mentions which one the tool picked.
This post walks through the core measures (mean, median, mode, variance, standard deviation, range, quartiles, IQR) with worked numbers. It also pins down the two places where implementations genuinely diverge: whether variance divides by n or n-1, and how quartiles get located in a sorted list. Both choices change the output, and both are easy to make silently. To keep it concrete, I will reference exactly what loopaloo's Statistics Calculator and CSV Statistics Generator compute, since the code makes the choices explicit.
The running dataset for most examples is the calculator's own "Example 1": 12, 15, 18, 21, 24, 27, 30, 15, 18, 21. Ten values, a couple of repeats, nothing exotic.
The three "averages" and when each one lies
"Average" is ambiguous. There are three common centers, and they answer different questions.
The mean is the sum divided by the count. For our ten numbers the sum is 201, so the mean is 201 / 10 = 20.1. In code this is exactly what you would expect:
const sum = nums.reduce((a, b) => a + b, 0);
const mean = sum / nums.length;
The mean uses every value, which is its strength and its weakness. One huge outlier drags it. If you append a single 300 to the set, the mean jumps from 20.1 to about 45.5 even though nine of the eleven numbers are under 30. That is the mean "lying": it reports a center that no actual data point is near.
The median is the middle value once the data is sorted. With an even count there is no single middle, so you average the two middle values. Sorted, our set is 12, 15, 15, 18, 18, 21, 21, 24, 27, 30. With ten elements the two middle slots are index 4 and index 5 (zero-based), holding 18 and 21, so the median is 19.5:
const sorted = [...nums].sort((a, b) => a - b);
const mid = Math.floor(sorted.length / 2);
const median = sorted.length % 2 === 0
? (sorted[mid - 1] + sorted[mid]) / 2
: sorted[mid];
The median ignores how extreme the extremes are; it only cares about ordering. Append that 300 and the median barely moves (to 21). That robustness is why income, home prices, and response-time latencies are almost always reported as medians. When a distribution is skewed, the median is the honest center.
The mode is the most frequent value. You build a frequency map and take the key(s) with the highest count:
const frequency = {};
nums.forEach((n) => { frequency[n] = (frequency[n] || 0) + 1; });
const maxFreq = Math.max(...Object.values(frequency));
const mode = Object.keys(frequency)
.filter((k) => frequency[parseFloat(k)] === maxFreq)
.map((k) => parseFloat(k));
In our set, 15, 18, and 21 each appear twice while everything else appears once, so there are three modes. The Statistics Calculator returns all of them and only shows "No mode" when every value is unique (when the number of modes equals the total count). The mode is the only one of the three that works on categorical data: the most common color, the most common error code. The CSV tool leans on exactly this. It computes a mode for every column, numeric or not, because a frequency count does not care about arithmetic. (One small difference: the CSV tool reports a single most frequent value per column rather than the full set of ties.)
Here is the quick rule of thumb for which center to trust:
| Situation | Best center | Why |
|---|---|---|
| Roughly symmetric, no outliers | Mean | Uses all data, efficient |
| Skewed or has outliers | Median | Resistant to extremes |
| Categories / non-numeric | Mode | Only one that is defined |
| You see mean far from median | Suspect skew | Compare them on purpose |
That last row is worth internalizing. The gap between mean and median is itself a signal. The calculator turns it into a number via Pearson's first skewness coefficient, 3 * (mean - median) / stdDev, and labels the result right-skewed, left-skewed, or symmetric. If mean greatly exceeds median, the tail points right.
Spread: variance, standard deviation, and the n vs n-1 question
A center tells you nothing about consistency. Test scores of [50, 50, 50] and [0, 50, 100] share a mean of 50 but could not be more different. Variance and standard deviation measure that spread.
Variance is the average of the squared distances from the mean. Squaring does two jobs: it makes every deviation positive (so they do not cancel) and it punishes large deviations more than small ones. Standard deviation is just the square root of variance, which brings the units back to the original scale (dollars instead of dollars-squared).
Here is where the single most common statistics gotcha lives. There are two variance formulas:
Population variance: sum((x - mean)^2) / n
Sample variance: sum((x - mean)^2) / (n - 1)
Dividing by n gives the population variance: the true spread when your data is the entire universe you care about. Dividing by n-1 gives the sample variance, used when your data is a sample drawn from a larger population and you want an unbiased estimate of that population's variance. The n-1 (called Bessel's correction) nudges the estimate upward, because a sample tends to cluster a little tighter around its own mean than around the unknown true mean, so dividing by n would underestimate.
Both loopaloo tools compute the population version, dividing by n:
// StatisticsCalculator.tsx
const variance = nums.reduce((acc, val) => acc + Math.pow(val - mean, 2), 0) / nums.length;
const stdDev = Math.sqrt(variance);
// CsvStatistics.tsx
variance = numericValues.reduce((acc, val) => acc + Math.pow(val - mean, 2), 0) / numericValues.length;
stdDev = Math.sqrt(variance);
That matters when you compare against a spreadsheet. Excel and Google Sheets have both: VAR.P/STDEV.P divide by n (population), while VAR.S/STDEV.S and the bare STDEV divide by n-1 (sample). If you paste a column into the CSV tool and the standard deviation does not match your sheet's STDEV, this is almost certainly why. Use STDEV.P for an apples-to-apples comparison.
Let me work the numbers for our set, mean 20.1. The squared deviations are:
| x | x - mean | (x - mean)^2 |
|---|---|---|
| 12 | -8.1 | 65.61 |
| 15 | -5.1 | 26.01 |
| 18 | -2.1 | 4.41 |
| 21 | 0.9 | 0.81 |
| 24 | 3.9 | 15.21 |
| 27 | 6.9 | 47.61 |
| 30 | 9.9 | 98.01 |
| 15 | -5.1 | 26.01 |
| 18 | -2.1 | 4.41 |
| 21 | 0.9 | 0.81 |
The squared deviations sum to 288.90. Population variance is 288.90 / 10 = 28.89, and standard deviation is sqrt(28.89) ≈ 5.375. If you wanted the sample variance instead, you would divide by 9: 288.90 / 9 = 32.10, giving a standard deviation of about 5.666. Same data, two legitimate answers, roughly an 11 percent difference in variance for ten points. The smaller n is, the bigger the gap, which is exactly when people get burned.
One derived measure the Statistics Calculator adds is the coefficient of variation, stdDev / |mean| * 100. It expresses spread as a percentage of the mean, so you can compare the consistency of two datasets on different scales (a process measured in millimeters versus one in kilometers). For our set that is roughly 5.375 / 20.1 * 100 ≈ 26.7%.
Range, quartiles, and IQR
The range is the bluntest spread measure: max - min. For our set, 30 - 12 = 18. It is trivially affected by a single outlier, so it is more a sanity check than an analysis.
Quartiles slice the sorted data into four parts. Q1 (the 25th percentile) is the value a quarter of the way up, Q2 is the median, and Q3 (the 75th percentile) is three quarters of the way up. The interquartile range is Q3 - Q1, the spread of the middle 50 percent of the data. Because IQR throws away the bottom and top quarters entirely, it is the most outlier-resistant spread measure there is.
The interesting wrinkle is that "the value a quarter of the way up" is not uniquely defined when n * 0.25 is not a whole number. There are several competing conventions (R alone ships nine of them), and they differ mainly in whether they interpolate between two neighboring data points or just pick an existing one. Both loopaloo tools take the simple, non-interpolating route: compute an index by flooring, then read that slot directly.
// Both tools use the same approach
const q1Index = Math.floor(sorted.length * 0.25);
const q3Index = Math.floor(sorted.length * 0.75);
const q1 = sorted[q1Index];
const q3 = sorted[q3Index];
For our ten sorted values 12, 15, 15, 18, 18, 21, 21, 24, 27, 30:
q1Index = floor(10 * 0.25) = floor(2.5) = 2, so Q1 issorted[2] = 15.q3Index = floor(10 * 0.75) = floor(7.5) = 7, so Q3 issorted[7] = 24.- IQR is
24 - 15 = 9.
This is a nearest-rank style percentile: the result is always an actual data point, never a value invented between two of them. A method that interpolates (like Excel's QUARTILE.INC, the default in most spreadsheets) would compute Q1 as a weighted blend of sorted[2] and sorted[3] and could return something like 15.75 instead of 15. Neither is "wrong"; they answer the question with different rounding rules. The thing to know is that loopaloo's quartiles are exact data points, so for small datasets they may differ from a spreadsheet by a fraction. The Statistics Calculator guards the smallest case explicitly: with a single value, Q1 and Q3 both collapse to that value rather than indexing past the array.
IQR also powers outlier detection. The classic Tukey rule flags anything below Q1 - 1.5 * IQR or above Q3 + 1.5 * IQR, and the Statistics Calculator implements exactly that:
const lowerBound = q1 - 1.5 * iqr;
const upperBound = q3 + 1.5 * iqr;
const outliers = sorted.filter(n => n < lowerBound || n > upperBound);
With Q1 = 15, Q3 = 24, IQR = 9, the fences sit at 15 - 13.5 = 1.5 and 24 + 13.5 = 37.5. Every value in our set is inside that window, so nothing is flagged. Add a 300 and it sails past 37.5 and gets reported as an outlier, which is the whole point: the bounds are derived from the resistant middle of the data, so a single extreme value cannot inflate them and hide itself.
Putting the picture together
No single number describes a distribution. The reason both tools report a whole panel at once (and draw a histogram) is that the measures are complementary:
- Center: mean for symmetric data, median when skewed, mode for categories.
- Spread: standard deviation for the typical deviation, IQR for the resistant middle, range as a quick bound.
- Shape: compare mean against median for skew direction; scan for outliers via the IQR fences.
A worked summary of our example, the way the calculator presents it:
| Measure | Value |
|---|---|
| Mean | 20.1 |
| Median | 19.5 |
| Mode | 15, 18, 21 |
| Range | 18 |
| Population variance | 28.89 |
| Population std dev | ~5.375 |
| Q1 / Q3 | 15 / 24 |
| IQR | 9 |
Mean (20.1) sitting just above median (19.5) hints at a faint right skew, and the three tied modes plus the clean histogram say this is a fairly flat, symmetric spread with no outliers. That entire read comes from numbers you can compute by hand on ten values, which is exactly why descriptive statistics is the first thing to run and the last thing to skip.
Try it on loopaloo
If you want to paste numbers and see all of this at once (mean, median, the three-way mode, population variance and standard deviation, quartiles, IQR, the 1.5x IQR outlier fences, plus a histogram, z-scores, and skewness), the Statistics Calculator does it live as you type. For tabular data, the CSV Statistics Generator runs the same population formulas per column, auto-detects which columns are numeric versus categorical, and gives every column a mode and frequency breakdown. Just remember that both compute population variance (divide by n) and non-interpolating quartiles, so if you are reconciling against a spreadsheet, reach for STDEV.P and expect small-dataset quartiles to differ by a fraction.
Related Tools
Related Articles
Try Our Free Tools
200+ browser-based tools for developers and creators. No uploads, complete privacy.
Explore All Tools