BitBootcampBitBootcamp ← all explainers 🔄 Review ⚙ Settings

DA-01-004-P · Primer — describe(): A Quick Numeric Summary

The idea in one sentence

df.describe() reads your whole table in one breath and hands back a small grid of numbers — count, mean, std, min, the quartiles, and max for every numeric column — so that before you plot a single thing you already know each column's scale, its spread, and where the surprises hide.

Why it exists

The very first question you ask of a new dataset is not "what's the average?" — it's the more humble "what am I even looking at?" Columns arrive at wildly different scales (an age near 40, a salary near 60,000, a tenure near 7), some are missing values you can't see, and one or two contain a single monster row that will bend every chart and every model you build later. Eyeballing thousands of rows to sense any of this is hopeless, and computing .mean(), .min(), .max() one column at a time is slow and easy to forget. describe() exists to collapse that whole reconnaissance step into one call: a compact summary table where each column is a column and each row is a statistic. It is the analyst's equivalent of turning on the lights before you walk into a room — you do it first, every time, so nothing surprises you later.

How it works

Call df.describe() and pandas walks every numeric column, ignoring the rest, and computes eight statistics down each one. count is how many non-missing values it found — your first honesty check. mean is the average and std the standard deviation, the typical distance of a value from that mean. min and max are the extremes, and the three percentiles — 25%, 50% (the median), 75% — are the quartiles, the values that carve the sorted data into four equal chunks. Reading it is a skill: when mean sits far above 50%, the data is skewed high; when max towers over 75%, you're looking at an outlier; when std dwarfs mean, the column is wildly spread. The table below shows exactly that story — watch the salary column:

import pandas as pd
import matplotlib.pyplot as plt

df = pd.DataFrame({
    "age":    [34, 41, 29, 52, 38, 45, 31, 60],
    "salary": [58000, 61000, 47000, 92000, 55000, 70000, 51000, 250000],
    "tenure": [3, 8, 1, 12, 5, 9, 2, 20],
})

summary = df.describe()
print(summary.round(1))
#         age    salary  tenure
# count   8.0       8.0     8.0   <- 8 rows, nothing missing
# mean   41.2   85500.0     7.5   <- salary mean...
# std    10.7   67930.6     6.3
# min    29.0   47000.0     1.0
# 25%    33.2   54000.0     2.8
# 50%    39.5   59500.0     6.5   <- ...but median is only 59,500 (mean >> median = skew!)
# 75%    46.8   75500.0     9.8
# max    60.0  250000.0    20.0   <- max dwarfs 75% -> a 250k outlier is pulling the mean up

# Chart the five-number summary so the spread and the outlier jump out:
five = summary.loc[["min", "25%", "50%", "75%", "max"], "salary"]
plt.bar(five.index, five.values, color="#4C72B0")
plt.title("salary: the jump from 75% to max is the outlier")
plt.ylabel("salary")
plt.tight_layout()
# The bar for "max" is ~3x the "75%" bar — the surprise is visible, not just numeric.

The table already told the story in numbers (mean 85,500 vs. median 59,500), and the bar chart makes the outlier impossible to miss — both from one describe() call.

The one gotcha

describe() silently ignores non-numeric columns and silently drops missing values — so what you see is not always what you have. If your table has a text column like city, it won't appear in the output at all; there's no warning, it's just gone, and a beginner can stare at the grid convinced they're seeing "the whole table" when they're seeing only the numeric slice. Worse, count reports non-null values, not row count — so if age has one blank, count reads 7 while your DataFrame has 8 rows, and every statistic below it was computed on the smaller, quieter set. Always compare each column's count against len(df): a count that comes up short is describe() waving a flag that missing data is present. (If you want the text columns summarized too — unique values, most frequent, etc. — ask for them explicitly with df.describe(include="all").) Read the table as a set of questions, not a verdict: Do the counts match? Does mean drift from median? Does max leap past 75%? Each mismatch is a lead worth chasing before you trust a single downstream number.

See it — reading each row of the summary as a question

flowchart TD
    A["df.describe()"] --> B["count row"]
    B --> C{"count == len(df)?"}
    C -->|"no"| D["missing values<br/>hide here"]
    C -->|"yes"| E["mean vs 50% row"]
    E --> F{"mean far above<br/>median?"}
    F -->|"yes"| G["skewed high"]
    F -->|"no"| H["max vs 75% row"]
    H --> I{"max dwarfs 75%?"}
    I -->|"yes"| J["outlier — chart it"]
    I -->|"no"| K["column looks<br/>well-behaved"]

The one-line takeaway

Run df.describe() first on every dataset, then read it as questions — do the counts match, does mean drift from median, does max leap past 75% — because each gap points straight at the missing values, skew, or outliers you'd otherwise discover the hard way.

Where you'll meet it

You'll meet describe() in the first ten seconds of every real analysis: fresh off pd.read_csv, before any plot or model, you run it to learn what scales you're dealing with and whether the file loaded clean. You'll meet it when a chart looks absurd — one bar a thousand times the rest — and describe() names the culprit as a max that dwarfs the 75%. You'll meet it when a total is mysteriously low and the count row reveals half the column was missing all along. And you'll meet it in interviews, where "here's a dataset, tell me what stands out" is answered fastest and most credibly by reading a summary table out loud: these are the scales, this column is skewed, that one has an outlier, and this one is missing values. Learning to read those eight rows fluently is the difference between guessing at your data and actually knowing it.

Named concepts: df.describe(), count/mean/std/min/max, reading the summary table