pandas value_counts() — count how often each value appears

The frequency table that should be your first command on any new column — counts, shares, missing data, and instant histograms in one call.

Every dataset lies by omission — value_counts() is the five-second audit that catches it.

What it does

value_counts() returns a Series mapping each distinct value to how often it appears, most frequent first. NaNs are excluded by default — flip dropna=False to count them. normalize=True gives proportions instead of raw counts, and bins=N buckets a numeric column into a quick histogram. The DataFrame version counts whole-row combinations (pass subset= to limit columns) — a groupby-size report in one line. Since pandas 2.0 the returned Series is named 'count'; since 3.0 ties keep a stable order and sort=False preserves first-appearance order instead of sorting by column values.

Why it matters

Real columns are messy, and value_counts() is the fastest honest survey of the mess: which category dominates, whether 'web ' and 'web' are both in there, how many rows are missing entirely. It drives everyday decisions — is this dataset imbalanced, does a 'rare' category even have enough rows to model, how big is the queue by status? Before you filter, group, or model anything, spend five seconds with value_counts(); it routinely kills assumptions you were about to build on.

Examples

import pandas as pd

tickets = pd.DataFrame({
    "ticket": range(1, 9),
    "status": ["open", "closed", "open", "pending",
               "open", "closed", "open", None],
})

print(tickets["status"].value_counts())
status
open       4
closed     2
pending    1
Name: count, dtype: int64

The support-queue triage: 4 open, 2 closed, 1 pending — and the 8th ticket's missing status silently vanished. dropna=True is the default; the missing row only shows up if you ask (next example).

survey = pd.DataFrame({
    "respondent": range(1, 6),
    "plan": ["pro", "free", None, "pro", "free"],
})

print(survey["plan"].value_counts(dropna=False))
plan
pro     2
free    2
NaN     1
Name: count, dtype: int64

dropna=False surfaces the skipped answer. For surveys and form data this is the difference between 'our two plans split 50/50' and 'a third of respondents never answered'.

orders = pd.DataFrame({
    "channel": ["web", "app", "web", "partner",
                "app", "web", "web", "app"],
})

print((orders["channel"].value_counts(normalize=True) * 100).round(1).to_string())
channel
web        50.0
app        37.5
partner    12.5

normalize=True returns proportions; multiply by 100 and round for a stakeholder-ready share column. No numpy, no loops, no pivot table.

errors = pd.Series(
    ["timeout", "auth", "timeout", "dns", "auth", "timeout"],
    name="error",
)
report = errors.value_counts().reset_index()
print(report.to_string(index=False))
  error  count
timeout      3
   auth      2
    dns      1

value_counts() returns a Series, not a DataFrame — .reset_index() converts it for CSV exports and reports. Since pandas 2.0 the count column arrives named 'count', so the header is already right.

Flags

FlagMeaning
normalizereturn proportions (sum to 1.0) instead of raw counts — multiply by 100 for percentages
sortTrue (default) orders by frequency; False keeps first-appearance order (behavior fixed and made stable in pandas 3.0)
ascendingTrue flips to rarest-first — the fastest way to find typos and one-off categories
binsinstead of counting values, cut a numeric column into N half-open bins — a one-line histogram
dropnaTrue (default) silently excludes NaN; False counts missing as its own row — flip it on survey data
subsetDataFrame version only: which columns define the combination being counted, e.g. subset=['region', 'device']
.reset_index()the standard idiom to turn the result back into a DataFrame with columns 'value-name' and 'count' for export

Counting since 0.5.0 (October 2011)

value_counts() is absent from the pandas 0.4.x sources but present in the v0.5.0 tag — released October 24, 2011 — as a hand-written defaultdict loop in series.py returning counts in descending order. The convenience knobs trickled in over the next years: normalize= around 0.11 (2013), sort/ascending/bins by 0.13 (2013).

The DataFrame version took seven years

Counting row combinations was GitHub issue #5377, opened in 2013, and it sat gathering upvotes until pandas 1.1.0 shipped DataFrame.value_counts() in July 2020. Pandas 3.0 then fixed the semantics: ties in the frequency sort are now stable, and sort=False preserves appearance order — before 3.0 it sorted by column values, and the tie order was unstable.

From a Python loop to a C hash table

The 2011 original was pure Python: a defaultdict loop over dropna().values, wrapped in a Series and sorted. Today Series.value_counts() dispatches into pandas' Cython layer — the khash hash table computes unique values and counts in one pass over the array, and the same machinery serves Series, Index, and the DataFrame row-combination version. Practical consequence: value_counts() is O(n) and fast, but if you only need the distinct values without counts, .unique() does strictly less work. On duplicate-heavy columns (few distinct values), a groupby-based count can also beat it, since groupby uses dictionary encoding that thrives on repetition.

Fun facts

Pros

Cons

Takeaways