5 Descriptive Statistics You Should Always Check First
The five descriptive statistics to check first are the mean versus the median, the standard deviation, the minimum and maximum, the quartiles (25th, 50th and 75th percentiles), and the count of non-missing values. Together they reveal skew, spread, impossible values and missing data before you start any deeper analysis.
Quick answer: Before any analysis, check (1) mean vs median for skew, (2) standard deviation for spread, (3) min and max for impossible values, (4) the 25th, 50th and 75th percentiles and the IQR (Q3 โ Q1), and (5) the non-missing count against the row count. In pandas, df.describe() returns all of these except the missing count, which df.isna().sum() gives you.
How do the mean and median show skewness?
Compare them: a mean well above the median signals right skew (a few high values pull the mean up), a mean below the median signals left skew, and a mean close to the median suggests a roughly symmetric distribution. For example, a $75,000 mean salary against a $58,000 median means a minority of high earners is lifting the average.
Mean: Average of all values (sensitive to outliers)
Median: Middle value (robust to outliers)
# Python
df['salary'].mean() # 75,000
df['salary'].median() # 58,000
What this tells you:
- Mean โ Median โ Symmetrical distribution
- Mean > Median โ Right-skewed (few high outliers pulling mean up)
- Mean < Median โ Left-skewed (few low outliers pulling mean down)
What does the standard deviation tell you?
The standard deviation measures how far values typically sit from the mean, in the same units as the data. A low standard deviation means values cluster near the mean; a high one means they are spread out.
# Python (pandas returns the sample standard deviation)
df['age'].std() # 12.5
# SQL: sample standard deviation, same as pandas
SELECT STDDEV_SAMP(age) FROM users;
Rules of thumb (for roughly normal data):
- ~68% of data within 1 std dev of mean
- ~95% within 2 std devs
- ~99.7% within 3 std devs
These percentages come from the normal distribution. For skewed data they do not hold, so describe the spread with percentiles instead. See standard deviation explained for how it is calculated.
Why should you check the min and max?
The minimum and maximum are the quickest way to catch impossible values, placeholder codes and data entry errors, such as a customer age of 0 or 150.
df['age'].min() # 0 โ Suspicious! Baby customers?
df['age'].max() # 150 โ Data entry error!
Check for:
- Impossible values (negative ages, future dates)
- Placeholder values (-1, 999, 0)
- Data entry errors (150-year-old person)
What do the 25th and 75th percentiles tell you?
Quartiles split the sorted data into four equal-sized groups: roughly a quarter of values fall below Q1 (the 25th percentile), half below the median, and three quarters below Q3 (the 75th percentile). More on the calculation in percentiles and quartiles explained.
# Python
df['price'].quantile([0.25, 0.5, 0.75])
# Result:
0.25 19.99 โ 25th percentile (Q1)
0.50 39.99 โ 50th percentile (median)
0.75 79.99 โ 75th percentile (Q3)
Interquartile Range (IQR) = Q3 - Q1
IQR tells you the range of the middle 50% of data (less affected by outliers than range). The common outlier rule flags values below Q1 โ 1.5 ร IQR or above Q3 + 1.5 ร IQR (see outlier detection).
How do you check the count and missing values?
Compare each column's non-missing count with the total number of rows: pandas' count() skips missing values, so any gap between the two is missing data.
# Python
df['email'].count() # 8,500
df['email'].isna().sum() # 1,500 missing
# Percentage missing
df['email'].isna().mean() * 100 # 15% missing
Questions to ask:
- Why are values missing? (Random or pattern?)
- Can I fill them or should I drop them?
- Is missing data informative? (e.g., "no response" is meaningful)
For what to do next, see handling missing data.
How do I get these statistics at once in pandas?
Run df.describe(): it returns the count, mean, standard deviation, min, quartiles and max for every numeric column in one table.
# Python: Get all at once
df.describe()
# Output:
age salary experience
count 10000.0 10000.0 10000.0
mean 35.5 75000.0 8.2
std 12.3 45000.0 5.1
min 18.0 25000.0 0.0
25% 27.0 45000.0 4.0
50% 34.0 58000.0 7.0
75% 43.0 95000.0 12.0
max 65.0 250000.0 30.0
How do you interpret describe() output?
Read it in the same order as the five checks. For these ten order values, the mean (61.70) is more than double the median (23.50), the standard deviation (119.19) is larger than the mean, and the max (400) is far above Q3 (33.75), which all point to at least one extreme value on the high end.
import pandas as pd
orders = pd.Series([12, 15, 18, 20, 22, 25, 30, 35, 40, 400])
print(orders.describe().round(2))
# count 10.00
# mean 61.70
# std 119.19
# min 12.00
# 25% 18.50
# 50% 23.50
# 75% 33.75
# max 400.00
# dtype: float64
The IQR is 33.75 โ 18.50 = 15.25, so the upper outlier fence is 33.75 + 1.5 ร 15.25 = 56.625, and 400 is the only value above it. Remove it and the mean falls from 61.7 to 24.1, while the median only moves from 23.5 to 22.
Red Flags to Look For
These patterns usually point to outliers, data quality problems or uneven missing data, and each one is worth investigating before you report a number.
- ๐ฉ Mean >> Median โ Check for outliers on high end
- ๐ฉ Min/Max unrealistic โ Data quality issues
- ๐ฉ Std dev > Mean โ Huge variability or outliers
- ๐ฉ High % missing โ Investigate data collection
- ๐ฉ Count differs by column โ Inconsistent missing data
What are common mistakes with descriptive statistics?
The most common mistake is mixing up the sample and population standard deviation: pandas' .std() divides by n โ 1 (sample), while NumPy's np.std() divides by n (population), so the same column gives two different answers.
import numpy as np
import pandas as pd
values = [2, 4, 4, 4, 5, 5, 7, 9]
print(round(pd.Series(values).std(), 2)) # 2.14 sample (ddof=1)
print(np.std(values)) # 2.0 population (ddof=0)
print(round(np.std(values, ddof=1), 2)) # 2.14 matches pandas
- SQL dialects differ:
STDDEVis the sample standard deviation in PostgreSQL but the population one in MySQL. WriteSTDDEV_SAMPorSTDDEV_POPto be explicit. - Text columns are skipped: on a DataFrame with mixed column types,
df.describe()summarizes only the numeric columns by default. Usedf.describe(include='all')to add count, unique, top and freq for text columns.
Golden Rule: Never skip descriptive statistics. Five minutes of checking these stats can save hours of debugging bad analysis later.
โ Back to Data Analysis Tips