Summarising a table
describe() is a checklist, not a report — run it on everything.
.describe().value_counts().nunique().agg().count()numeric_onlyWatch it happen
Play it through, or step back and forth yourself.
dfdf["cups"].sum()615df["cups"].mean()102.5df["cups"].max()150df.mean(numeric_only=True)one value per columnEvery aggregation you met in NumPy works on a Series: .sum(), .mean(), .min(), .max(), .std(). On a DataFrame they run per column and hand you back a Series.
The idea
Every aggregation from the NumPy track works here. On a Series they give one number; on a DataFrame they run per column and hand back a Series indexed by column name:
orders["cups"].sum() # one number
orders.sum(numeric_only=True) # one per numeric column
orders.mean(numeric_only=True)numeric_only=True matters on a mixed table — without it, summing a text column either concatenates the strings or raises, depending on the operation.
describe as a checklist
orders.describe() runs eight summaries at once for every numeric column. Its value isn't the numbers themselves — it's that the rows form a checklist for problems you haven't thought to look for yet.
count counts non-missing values. Two columns with different counts means one has gaps, and you found them without going looking.
min and max are where nonsense lives. An age of -1, a price of 999999, a date in 1900 — sentinel values and parsing failures sit at the extremes, and this puts them in front of you.
mean against 50% is your skew check. When they diverge, the distribution has a tail and every average you report afterwards is misleading — that's the lesson-21 argument from the NumPy track, applied automatically to every column.
std of zero means a constant column, which is usually a bug or a column you can drop.
orders.describe() # numeric columns
orders.describe(include="all") # text too: count, unique, top, freq
orders.describe(include="object") # only the text columns
Counting categories
describe is for numbers. For a text or categorical column you want counts:
orders["city"].value_counts() # how many of each
orders["city"].value_counts(normalize=True) # as proportions
orders["city"].value_counts(dropna=False) # count the gaps too
orders["city"].nunique() # 3 — how many distinctvalue_counts sorts by frequency descending, so the most common value is at the top. That's usually what you want — and it's how you spot a category that appears twice under two spellings, which is what lesson 11 was about.

Naming your own summaries
orders.agg(["min", "max", "mean"], numeric_only=True)
orders.agg({"cups": "sum", "revenue": "mean", "rating": "max"}).agg() takes a list to apply several functions, or a dict to apply a different one per column. It also takes your own function, which is how you'd add a percentile or a custom metric to the same table.
All of these skip missing values by default — the same choice lesson 9 made explicit. If you want the gaps to propagate instead, do the arithmetic yourself.
Practice
Write it yourself. The answer is there when you want it.
Putting the kettle on…
Starting up…
Write it yourself
not gradedPrint describe() on orders, then value_counts() on city, then the same on item as proportions rounded to three places. Finish with one agg asking three columns three different questions — total cups, mean revenue, best rating.
Your turn
4 exercises. Write the code yourself, then press Check — a nudge and the answer are there if you want them.
Return the summary statistics for the numeric columns of orders.
How many orders came from each city? Return the counts.
How many distinct items appear in orders? Return the number.
Return the total cups and the mean revenue in one call, using .agg() with a dict.
