Pandas value_counts(): Frequencies and Percentages

Use value_counts() to count values in a column or combinations of columns. Choose a missing-value policy, state the denominator for percentages and normalise labels before building a frequency report.

Count one column or combinations of columns 🔝

Use Series.value_counts() for one column and DataFrame.value_counts(subset=...) for combinations. Both return frequencies. The method name is value_counts(). count() counts non-missing entries, while unique() returns distinct values without frequencies. Only the Series version accepts bins.

Count student classes 🔝

The synthetic table contains Three three times, Four twice and Five once. The result has values as its index and counts as its data.

import pandas as pd
import numpy as np
def student_data():
    return pd.DataFrame({"NAME": ["Ravi", "Raju", "Alex", "Ron", "King", "Jack"], "ID": [1, 2, 3, 4, 5, 6], "MATH": [80, 40, 70, 70, 60, 30], "CLASS1": ["Four", "Three", "Three", "Four", "Five", "Three"]})
df = student_data()
counts = df["CLASS1"].value_counts()
print(counts)
assert counts.to_dict() == {"Three": 3, "Four": 2, "Five": 1}

Expected output

CLASS1
Three    3
Four     2
Five     1
Name: count, dtype: int64

Convert frequencies to proportions and percentages 🔝

normalize=True divides by the number of included observations. Multiply by 100 for percentages. Rounding displayed percentages can make their sum differ slightly from 100.

classes = student_data()["CLASS1"]
proportions = classes.value_counts(normalize=True)
print(proportions)
print("Percentages:")
print(proportions.mul(100).round(2))
assert np.isclose(proportions.sum(), 1)
assert proportions.loc["Three"] == 0.5

Expected output

CLASS1
Three    0.500000
Four     0.333333
Five     0.166667
Name: proportion, dtype: float64
Percentages:
CLASS1
Three    50.00
Four     33.33
Five     16.67
Name: proportion, dtype: float64

Choose frequency order or label order 🔝

sort=False avoids sorting by frequency. For a deliberate alphabetical report, use sort_index(). ascending=True sorts frequencies from smallest to largest. Do not rely on ties to communicate a business priority.

classes = student_data()["CLASS1"]
print("No frequency sort:")
print(classes.value_counts(sort=False))
print("Alphabetical:")
print(classes.value_counts().sort_index())
print("Lowest frequency first:")
print(classes.value_counts(ascending=True))
assert classes.value_counts(ascending=True).tolist() == [1, 2, 3]

Expected output

No frequency sort:
CLASS1
Four     2
Three    3
Five     1
Name: count, dtype: int64
Alphabetical:
CLASS1
Five     1
Four     2
Three    3
Name: count, dtype: int64
Lowest frequency first:
CLASS1
Five     1
Four     2
Three    3
Name: count, dtype: int64

Include missing values and check the denominator 🔝

dropna=True excludes missing values by default. dropna=False counts them. With normalization the choice changes the denominator, so state whether a percentage is of known values or all records.

values = pd.Series(["paid", "paid", "test", pd.NA], dtype="string")
known = values.value_counts()
all_rows = values.value_counts(dropna=False)
print("Known values:")
print(known)
print("Include missing:")
print(all_rows)
print("Known proportion:", values.value_counts(normalize=True).loc["paid"])
print("All-row proportion:", values.value_counts(normalize=True, dropna=False).loc["paid"])
assert known.sum() == 3 and all_rows.sum() == 4
assert values.value_counts(normalize=True, dropna=False).loc["paid"] == 0.5

Expected output

Known values:
paid    2
test    1
Name: count, dtype: Int64
Include missing:
paid    2
test    1
<NA>    1
Name: count, dtype: Int64
Known proportion: 0.6666666666666666
All-row proportion: 0.5

Count repeated marks with and without missing values 🔝

Use a fresh student table and make King’s mark missing. The repeated score 70 occurs twice. Equal-frequency scores may have a different display order across versions; check the counts rather than assuming a tie order.

df = student_data()
df["MATH"] = df["MATH"].astype("Float64")
df.loc[4, "MATH"] = pd.NA
print(df["MATH"].value_counts(dropna=False))
print("Exclude missing:")
print(df["MATH"].value_counts(dropna=True))
assert df["MATH"].value_counts().loc[70] == 2
assert df["MATH"].value_counts(dropna=False).sum() == 6
assert df["MATH"].value_counts().sum() == 5

Expected output

MATH
70.0    2
80.0    1
40.0    1
30.0    1
<NA>    1
Name: count, dtype: Int64
Exclude missing:
MATH
70.0    2
80.0    1
40.0    1
30.0    1
Name: count, dtype: Int64

Count numeric intervals with Series bins 🔝

The Series bins option groups numeric observations into intervals. Integer bins create equal-width intervals from the data range. An edge sequence creates specified widths. For labels and explicit boundary rules use pd.cut() followed by value_counts().

marks = student_data()["MATH"]
print("Three equal-width bins:")
print(marks.value_counts(bins=3).sort_index())
print("Specified edges:")
fixed = marks.value_counts(bins=[1, 50, 70, 90]).sort_index()
print(fixed)
assert fixed.tolist() == [2, 3, 1]
assert fixed.sum() == 6

Expected output

Three equal-width bins:
(29.948999999999998, 46.667]    2
(46.667, 63.333]                1
(63.333, 80.0]                  3
Name: count, dtype: int64
Specified edges:
(0.999, 50.0]    2
(50.0, 70.0]     3
(70.0, 90.0]     1
Name: count, dtype: int64

Count distinct combinations with a DataFrame 🔝

Select the fields that define a combination. Including an order ID that is unique on every row would usually make each full row appear only once. The output for multiple columns uses a MultiIndex.

orders = pd.DataFrame({"order_id": [1, 2, 3, 4, 5], "product": ["pen", "pen", "book", "pen", "book"], "status": ["paid", "paid", "paid", "test", "paid"]})
counts = orders.value_counts(subset=["product", "status"])
print(counts)
print("Flat table:")
print(counts.rename("orders").reset_index())
assert counts.loc[("pen", "paid")] == 2
assert counts.sum() == len(orders)

Expected output

product  status
pen      paid      2
book     paid      2
pen      test      1
Name: count, dtype: int64
Flat table:
  product status  orders
0     pen   paid       2
1    book   paid       2
2     pen   test       1

Control missing rows in selected combinations 🔝

DataFrame.value_counts() normally omits rows missing any selected field. Unselected fields do not affect that omission. dropna=False includes missing combinations. Pandas 3 preserves data order with sort=False; older versions sorted combination labels.

orders = pd.DataFrame({"product": ["pen", "pen", None], "status": ["paid", None, "paid"], "note": [None, None, None]})
known = orders.value_counts(subset=["product", "status"])
all_rows = orders.value_counts(subset=["product", "status"], dropna=False, sort=False)
print("Complete combinations:")
print(known)
print("Include missing combinations:")
print(all_rows)
assert known.sum() == 1
assert all_rows.sum() == 3

Expected output

Complete combinations:
product  status
pen      paid      1
Name: count, dtype: int64
Include missing combinations:
product  status
pen      paid      1
         NaN       1
NaN      paid      1
Name: count, dtype: int64

Show defined categories with zero counts 🔝

Use a categorical dtype or reindex to a reporting list when a category must appear even if absent. Check unexpected labels first: converting them to a fixed categorical dtype can turn them into missing values.

values = pd.Series(["Low", "High", "Low"], dtype=pd.CategoricalDtype(["Low", "Medium", "High"], ordered=True))
counts = values.value_counts(sort=False)
print(counts)
assert counts.to_dict() == {"Low": 2, "Medium": 0, "High": 1}
assert counts.index.tolist() == ["Low", "Medium", "High"]

Expected output

Low       2
Medium    0
High      1
Name: count, dtype: int64

Normalise product labels before summarising sales 🔝

This synthetic extract includes spelling case, surrounding spaces, a blank product and missing values. Keep raw labels. Trim text and use casefold() as a comparison key before counting. This combines text variants only; it does not prove two differently named products are the same product.

raw = pd.DataFrame({"order_id": [101, 102, 103, 104, 105, 106], "product": [" Pen ", "PEN", "Book", " ", None, "pen"], "status": ["paid", "paid", "paid", "paid", "paid", "test"], "amount": [5, 7, 10, 3, 4, 2]})
cleaned = raw.copy()
product = raw["product"].astype("string").str.strip()
cleaned["product_key"] = product.mask(product.eq("")).str.casefold()
paid = cleaned.loc[cleaned["status"].eq("paid")].copy()
counts = paid["product_key"].value_counts(dropna=False)
print("Paid orders, including missing products:")
print(counts)
print("Review original labels:")
print(raw.loc[cleaned["product_key"].isna()])
assert counts.loc["pen"] == 2
assert counts.sum() == 5
assert cleaned["product_key"].isna().sum() == 2

Expected output

Paid orders, including missing products:
product_key
pen     2
<NA>    2
book    1
Name: count, dtype: Int64
Review original labels:
   order_id product status  amount
3       104           paid       3
4       105     NaN   paid       4

Separate order counts from revenue and export 🔝

value_counts() counts occurrences, not revenue. Use groupby() for amount aggregation. This report uses only known product keys; its percentages therefore describe those orders, not all paid orders. Missing products remain in the review count.

known_paid = paid.loc[paid["product_key"].notna()].copy()
summary = known_paid["product_key"].value_counts().rename("orders").to_frame()
summary["percent_of_known"] = summary["orders"].div(summary["orders"].sum()).mul(100).round(2)
summary["revenue"] = known_paid.groupby("product_key")["amount"].sum()
print(summary)
print("Paid orders needing product review:", int(paid["product_key"].isna().sum()))
print(summary.reset_index().to_csv(index=False).rstrip())
assert summary.loc["pen", "orders"] == 2
assert summary.loc["pen", "revenue"] == 12
assert summary["orders"].sum() == 3
# Optional Colab export:
# summary.reset_index().to_csv("product_frequency_report.csv", index=False)

Expected output

             orders  percent_of_known  revenue
product_key                                   
pen               2             66.67       12
book              1             33.33       10
Paid orders needing product review: 2
product_key,orders,percent_of_known,revenue
pen,2,66.67,12
book,1,33.33,10

Common questions 🔝

Why are blank strings counted? They are values until you turn blanks into missing values. Why do percentages change with dropna? The denominator changes. Why are row combinations all counted once? You may have included a unique ID. Does normalization count unique customers? It counts included observations; define and deduplicate customer records first when needed. Does counting correct spelling? No; define a normalisation or mapping rule. Does bins count missing amounts? Audit missing and out-of-range values separately; use cut() when you need explicit band rules. Continue with case conversion, duplicate removal and bar charts.

Exercise: count all statuses and calculate a share 🔝

For statuses paid, test, paid and a missing value, include missing values in the count. Calculate paid as a percentage of all four observations, then compare the percentage of known observations.

Exercise solution 🔝

The paid share is 50% of all observations and about 66.67% of known observations.

status = pd.Series(["paid", "test", "paid", pd.NA], dtype="string")
counts = status.value_counts(dropna=False)
all_percent = status.value_counts(normalize=True, dropna=False).loc["paid"] * 100
known_percent = status.value_counts(normalize=True).loc["paid"] * 100
print(counts)
print("All observations:", round(all_percent, 2))
print("Known observations:", round(known_percent, 2))
assert all_percent == 50
assert np.isclose(known_percent, 200 / 3)

Expected output

paid    2
test    1
<NA>    1
Name: count, dtype: Int64
All observations: 50.0
Known observations: 66.67

Practice in Google Colab 🔝

Open in Google Colab View on GitHub
Run the examples in order, change categories and missing-value settings, then compare frequencies and percentages. Save your own copy to keep edits. All sample tables are included in the notebook.

Continue with the Data Cleaning hub, where(), loc, at, NumPy where(), fillna() and iloc.

Reference: Pandas DataFrame.value_counts documentation.




Subscribe to our YouTube Channel here



plus2net.com







Python Video Tutorials
Python SQLite Video Tutorials
Python MySQL Video Tutorials
Python Tkinter Video Tutorials
✖
We use cookies to improve your browsing experience. . Learn more
HTML MySQL PHP JavaScript ASP Photoshop Articles Contact us
© 2000-2026 plus2net.com All rights reserved worldwide Privacy Policy Disclaimer