Counting & Distributions
Count how often each value appears with value_counts() (also as percentages), count distinct values with nunique(), and know size() vs count().
Some of the most common analysis questions are just counting: "Which team won the most matches?", "What % of captains chose to field after winning the toss?", "How many different winners were there?". In the IPL notebook (06) Munna used exactly these: ipl["winner"].value_counts() and groupby(...).count().
Our match data for this topic (one winner is missing — a no-result):
python
m = pd.DataFrame({
"season": [2022, 2022, 2022, 2023, 2023, 2023, 2023, 2024, 2024, 2024],
"winner": ["GT", "RR", "GT", "CSK", "GT", "CSK", "MI", "KKR", "KKR", None],
"toss_decision": ["field", "bat", "field", "field", "bat", "field", "field", "bat", "field", "field"],
})
print(m)text
season winner toss_decision
0 2022 GT field
1 2022 RR bat
2 2022 GT field
3 2023 CSK field
4 2023 GT bat
5 2023 CSK field
6 2023 MI field
7 2024 KKR bat
8 2024 KKR field
9 2024 NaN fieldDefinition
value_counts()returns a Series containing the count of each unique value, sorted from most to least frequent; withnormalize=Trueit returns proportions (fractions that add up to 1) instead of counts.
nunique()returns the number of distinct (unique) values, ignoringNaNby default.On a GroupBy,
size()counts the rows in each group, whilecount()counts the non-missing values of each column in each group.
- Unique value — each different value (
"GT","CSK"…) becomes one index label of the result. - Distribution — the table of "value → how often" tells you how the data is spread across categories.
- Proportion — count ÷ total non-missing values. Multiply by 100 for a percentage.
Syntax
python
Series.value_counts(normalize=False, sort=True, ascending=False, bins=None, dropna=True)
DataFrame.value_counts(subset=None, normalize=False, sort=True, ascending=False, dropna=True)
Series.nunique(dropna=True)
DataFrame.nunique(axis=0, dropna=True)
df.groupby(key).size()
df.groupby(key).count()| Parameter | Meaning | Default |
|---|---|---|
normalize | return proportions (0–1) instead of counts | False |
sort | sort by frequency; False keeps order of first appearance | True |
ascending | sort least-frequent first | False |
bins | (numeric Series) cut values into this many equal-width bins and count each bin | None |
dropna | True ignores NaN; False counts NaN as its own value | True |
subset | (DataFrame) columns whose combinations are counted | all columns |
Example — counts and percentages
python
print(m["winner"].value_counts())
print((m["winner"].value_counts(normalize=True) * 100).round(1))text
winner
GT 3
CSK 2
KKR 2
RR 1
MI 1
Name: count, dtype: int64
winner
GT 33.3
CSK 22.2
KKR 22.2
RR 11.1
MI 11.1
Name: proportion, dtype: float64How it works: the unique winners become the index (named after the column, winner) and the Series is named count — or proportion with normalize=True. The missing winner is ignored, so the proportions are out of 9, not 10 (3/9 = 33.3%).
Note: since pandas 2.0 the result Series is named
count/proportionand its index is named after the original column. Older tutorials show the column name as the Series name.
Example — including NaN, and turning the result into a table
python
print(m["winner"].value_counts(dropna=False))
print(m["winner"].value_counts().reset_index())text
winner
GT 3
CSK 2
KKR 2
RR 1
MI 1
NaN 1
Name: count, dtype: int64
winner count
0 GT 3
1 CSK 2
2 KKR 2
3 RR 1
4 MI 1.head(1) gives the most frequent value; value_counts(ascending=True) the rarest first; sort=False keeps first-appearance order.
Example — counting combinations
python
print(m[["season", "toss_decision"]].value_counts())text
season toss_decision
2023 field 3
2022 field 2
2024 field 2
2022 bat 1
2023 bat 1
2024 bat 1
Name: count, dtype: int64DataFrame.value_counts() counts each unique row combination. The same idea per group is m.groupby("season")["winner"].value_counts(), and with normalize=True you get the share within each season:
python
print(m.groupby("season")["toss_decision"].value_counts(normalize=True).round(2))text
season toss_decision
2022 field 0.67
bat 0.33
2023 field 0.75
bat 0.25
2024 field 0.67
bat 0.33
Name: proportion, dtype: float64Example — nunique() and unique()
python
print(m["winner"].nunique())
print(m["winner"].nunique(dropna=False))
print(m["winner"].unique())
print(m.nunique())
print(m.groupby("season")["winner"].nunique())text
5
6
<StringArray>
['GT', 'RR', 'CSK', 'MI', 'KKR', nan]
Length: 6, dtype: str
season 3
winner 5
toss_decision 2
dtype: int64
season
2022 2
2023 3
2024 1
Name: winner, dtype: int64How it works: unique() lists the distinct values (in order of appearance, including nan); nunique() just counts them (excluding NaN unless dropna=False). Grouped, it answers "how many different winners per season?".
Note (pandas 3):
unique()on a text column returns aStringArraywith dtypestrinstead of a NumPy object array. Use.tolist()if you need a plain list.
Example — size() vs count()
python
print(m.groupby("season").size())
print(m.groupby("season").count())text
season
2022 3
2023 4
2024 3
dtype: int64
winner toss_decision
season
2022 3 3
2023 4 4
2024 2 3How it works: 2024 has 3 matches (size) but only 2 known winners (count skips the NaN). size() returns one Series (rows per group); count() returns one column per column.
Comparison
| Tool | Answers | NaN | Returns |
|---|---|---|---|
s.value_counts() | how often each value appears | skipped (dropna=False to count) | Series value → count |
s.nunique() | how many distinct values | skipped by default | int |
s.unique() | which distinct values | included | array |
s.count() | how many non-missing values | skipped | int |
len(s) / s.size | how many slots | included | int |
groupby().size() | rows per group | included | Series |
groupby().count() | non-missing per column per group | skipped | DataFrame |
Careful:
DataFrame.size(an attribute, no brackets) is rows × columns —m.sizeis30— not the number of rows. Uselen(m)orm.shape[0]for rows.
Common mistakes
- ❌ Reading
value_counts(normalize=True)as percentages. ✅ They are fractions; multiply by 100. - ❌ Forgetting that
NaNis excluded and the shares are of non-missing values only. ✅ Usedropna=Falseto see it. - ❌ Using
groupby().count()to count matches when some columns have NaN. ✅ Usegroupby().size()for row counts. - ❌
m.sizeto get the number of rows. ✅len(m).
Interview tip
Q: How do you get the percentage share of each category in a column?
A: df["col"].value_counts(normalize=True) * 100. Add dropna=False if missing values should count as a category.
Key points to remember
- value_counts() counts each unique value, sorted most-frequent first; the result Series is named 'count'.
- value_counts(normalize=True) returns proportions (named 'proportion'); multiply by 100 for percentages.
- NaN is ignored by value_counts() and nunique() unless dropna=False.
- DataFrame.value_counts() and groupby(...)[col].value_counts() count combinations of values.
- nunique() counts distinct values; unique() lists them (a str-dtype array for text in pandas 3).
- groupby().size() counts rows per group; groupby().count() counts non-missing values per column.
- The DataFrame.size attribute is rows × columns, not the row count — use len(df).
Try it yourself
Match winners: counts, percentages and NaN
Per-season counts: size, count, nunique
Practice: how do batters get out?
Revision Flashcards
Tap a card to flip it and see the answer.
