Counting & Uniques: value_counts, unique, nunique
Reviewed & published by Brayan K
value_counts() tallies how often each distinct value appears in a column, giving you an instant frequency table — the workhorse of exploratory data analysis.
Part of the free Pandas course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
Learn to count categories, turn counts into percentages with normalize=, include missing values with dropna=False, and list distinct values with unique() and nunique().
What You'll Learn in This Lesson
- Build a frequency table with value_counts()
- Turn counts into shares with normalize=True
- Reveal missing values with dropna=False
- List distinct values with .unique()
- Count distinct values with .nunique()
- Use these for fast exploratory analysis (EDA)
1 The Frequency Table — value_counts()
Call value_counts() on a Series and pandas counts how many times each distinct value appears, then hands back the tallies sorted from most common to least. It is the first thing analysts run on a new categorical column: one line tells you the mode, the spread, and whether any category dominates.
import pandas as pd
city = pd.Series(["NYC", "LA", "NYC", "NYC", "LA", "SF"])
city.value_counts()
# NYC 3
# LA 2
# SF 1
# Name: count, dtype: int64import pandas as pd
city = pd.Series(["NYC", "LA", "NYC", "NYC", "LA", "SF"])
counts = city.value_counts()
print(counts)
# The most common value (the mode) is the first row:
print("most common:", counts.index[0])
# most common: NYC
# ✅ Expected output:
# NYC 3
# LA 2
# SF 1
# Name: count, dtype: int64
# most common: NYC2 Shares & Missing — normalize= and dropna=
Two arguments make value_counts far more useful. normalize=True converts the raw counts into proportions that add up to 1 — multiply by 100 and you have percentages, perfect for "what share of customers are on each plan?" And dropna=False stops pandas from hiding the gaps: missing values appear as their own NaN row so you can see how incomplete a column really is.
plan = pd.Series(["free", "pro", "free", None, "pro", "free"])
plan.value_counts(normalize=True) # shares of the non-null values
plan.value_counts(dropna=False) # show the NaN count too
(plan.value_counts(normalize=True) * 100).round(1) # percentagesimport pandas as pd
plan = pd.Series(["free", "pro", "free", None, "pro", "free"])
# Shares as percentages (NaN dropped by default)
print((plan.value_counts(normalize=True) * 100).round(1))
# Now reveal the missing value
print(plan.value_counts(dropna=False))
# free 3
# pro 2
# NaN 1
# Name: count, dtype: int64
# ✅ Expected output:
# free 60.0
# pro 40.0
# Name: proportion, dtype: float64
# free 3
# pro 2
# None 1
# Name: count, dtype: int643 Distinct Values — unique() and nunique()
When you do not need the counts, just the distinct values, reach for unique() and nunique(). unique() returns an array of the actual distinct values — useful for spotting typos and stray categories. nunique() returns a single number: how many distinct values exist. That count, called cardinality, tells you whether a column is a good candidate for the category dtype or a grouping key.
dept = pd.Series(["sales", "ops", "sales", "ops", "ops"])
dept.unique() # array(['sales', 'ops'], dtype=object)
dept.nunique() # 2
dept.nunique(dropna=False) # also count NaN as a distinct valueimport pandas as pd
dept = pd.Series(["sales", "ops", "sales", "ops", "ops", "sales"])
print(dept.unique())
print("distinct count:", dept.nunique())
# distinct count: 2
print("total rows:", len(dept))
# total rows: 6 -> lots of repeats, a good 'category' candidate
# ✅ Expected output:
# ['sales' 'ops']
# distinct count: 2
# total rows: 6import pandas as pd
s = pd.Series(["yes", "no", "yes", "yes", "no"])
# 🎯 YOUR TURN:
# Show what SHARE of responses each answer is (as proportions 0-1).
# Fill in the argument that turns counts into proportions.
print(s.value_counts(___=True))
# Expected output:
# yes 0.6
# no 0.4
# Name: proportion, dtype: float64! Common Errors (And How to Fix Them)
❌ Calling value_counts on a whole DataFrame by mistake
You wanted to count one column but passed the frame:
df.value_counts() # ❌ counts whole-row combinations, not one column✅ Fix: select the column first:
df["city"].value_counts() # ✅❌ Missing values mysteriously absent
Your counts do not add up to the row total because NaN was dropped:
s.value_counts() # ❌ silently ignores NaN✅ Fix: include them explicitly:
s.value_counts(dropna=False) # ✅ NaN gets its own row🎯 Mini Challenge: Profile a Survey Column
Quickly profile a column of responses for an EDA report.
- Count each response
- Show the percentage share of each
- Report how many distinct answers there were
- Count how many values are missing
import pandas as pd
resp = pd.Series(["agree", "agree", "disagree", None, "agree", "neutral"])
print(resp.value_counts())
# agree 3
# disagree 1
# neutral 1
# Name: count, dtype: int64
print((resp.value_counts(normalize=True) * 100).round(1))
# agree 60.0
# disagree 20.0
# neutral 20.0
print("distinct:", resp.nunique())
# distinct: 3
print("missing:", resp.isna().sum())
# ✅ Expected output:
# agree 3
# disagree 1
# neutral 1
# Name: count, dtype: int64
# agree 60.0
# disagree 20.0
# neutral 20.0
# Name: proportion, dtype: float64
# distinct: 3
# missing: 1❓ Frequently Asked Questions
Lesson complete — you can profile any column!
You can build frequency tables with value_counts(), switch to shares with normalize=True, expose gaps with dropna=False, and measure distinctness with unique() and nunique().
Practice quiz
What does s.value_counts() return?
- The number of rows
- A frequency table of how often each value appears
- The unique values only
- The column mean
Answer: A frequency table of how often each value appears. value_counts() tallies each distinct value's frequency.
How is value_counts() ordered by default?
- Alphabetically
- By the index labels
- From most frequent to least frequent
- Randomly
Answer: From most frequent to least frequent. It sorts descending by count, so the top row is the mode.
What does normalize=True do?
- Sorts the result
- Removes missing values
- Capitalises labels
- Converts counts to proportions that sum to 1
Answer: Converts counts to proportions that sum to 1. normalize=True returns each value's share as a fraction.
Which argument makes NaN appear as its own row?
- dropna=False
- include_na=True
- keepna=True
- na='show'
Answer: dropna=False. dropna=False stops pandas hiding the missing values.
Does value_counts() include NaN by default?
- Always
- No, it drops NaN unless told otherwise
- Only for numeric columns
- Only the first NaN
Answer: No, it drops NaN unless told otherwise. By default NaN is excluded; pass dropna=False to include it.
What does unique() return?
- A single count
- The most common value
- An array of the distinct values
- A sorted Series
Answer: An array of the distinct values. unique() lists the actual distinct values in the column.
What does nunique() return?
- The distinct values
- A frequency table
- The first value
- The count of distinct values (a single number)
Answer: The count of distinct values (a single number). nunique() reports how many distinct values exist (cardinality).
If value_counts(normalize=True) is computed with dropna=True, what is the denominator?
- Only the non-missing rows
- All rows including NaN
- Always 100
- The number of columns
Answer: Only the non-missing rows. Proportions are over non-null values unless you keep NaN in.
What is the result type of value_counts()?
- A list
- A dict
- A Series (values as index, counts as values)
- A DataFrame
Answer: A Series (values as index, counts as values). It returns a Series, so you can chain .head() or .plot.bar().
A low nunique() relative to row count suggests what?
- The data is corrupted
- Every value is unique
- There are no repeats
- Lots of repeats — a good 'category' dtype candidate
Answer: Lots of repeats — a good 'category' dtype candidate. Few distinct values among many rows means heavy repetition.
Continue this course
- Previous: Renaming & Relabelling
- Next: Filtering with query() & eval() — Write readable filters and computed columns with query() and eval()
- Quick reference: Pandas cheat sheet
Frequently asked questions
What is the difference between unique() and nunique()?
unique() returns an array of the actual distinct values in a column, so you can see WHAT they are. nunique() returns just the count of distinct values — a single number telling you HOW MANY there are. Use unique() to inspect the values and nunique() to measure cardinality.
What does normalize=True do in value_counts?
By default value_counts() gives raw counts. normalize=True converts those counts into proportions that sum to 1, so each value's share is shown as a fraction (multiply by 100 for percentages). It is the quickest way to see the relative mix of categories without doing the division yourself.
Does value_counts include missing values?
No — by default it drops NaN. Pass dropna=False to make NaN appear as its own row in the result, which is essential during data cleaning when you need to know how many values are actually missing in a column.
How is value_counts ordered?
By default it sorts from most frequent to least frequent, so the top row is the mode. Pass sort=False to keep the values in the order pandas finds them, or chain .sort_index() to order by the value labels themselves instead of by frequency.