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

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: int64
import 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: NYC

2 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)   # percentages
import 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: int64

3 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 value
import 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: 6
import 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.

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

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.