Pandas unique() and nunique(): Distinct Values and Counts

Use unique() to list distinct values and nunique() to count them. Choose how to treat missing data and normalize labels before interpreting customer or category counts.

Create the student dataset 🔝

Select a column to use Series.unique(). A DataFrame has nunique(), but does not have a unique() method.

import pandas as pd

students = pd.DataFrame({
    "NAME": ["Ravi", "Raju", "Alex", "Ron", "King", "Jack"],
    "ID": [1, 2, 3, 4, 5, 6],
    "GAME": ["CRICKET", "TENNIS", "CRICKET", "HOCKEY", "CRICKET", "TENNIS"],
    "CLASS1": ["Four", "Three", "Three", "Four", "Five", "Three"]
})
print(students)

Expected output

   NAME  ID     GAME CLASS1
0  Ravi   1  CRICKET   Four
1  Raju   2   TENNIS  Three
2  Alex   3  CRICKET  Three
3   Ron   4   HOCKEY   Four
4  King   5  CRICKET   Five
5  Jack   6   TENNIS  Three

Find distinct class names 🔝

unique() preserves first appearance; it does not sort the result or return frequencies. It takes no values parameter when called on a Series.

classes = students["CLASS1"].unique()
print(classes)
assert classes.tolist() == ["Four", "Three", "Five"]

Expected output

<StringArray>
['Four', 'Three', 'Five']
Length: 3, dtype: str

Find distinct games 🔝

Use tolist() when a Python list is convenient. Depending on the Series dtype, unique() returns a NumPy array or an extension array.

games = students["GAME"].unique()
print(games)
print("As a list:", games.tolist())
assert games.tolist() == ["CRICKET", "TENNIS", "HOCKEY"]

Expected output

<StringArray>
['CRICKET', 'TENNIS', 'HOCKEY']
Length: 3, dtype: str
As a list: ['CRICKET', 'TENNIS', 'HOCKEY']

Count distinct values with nunique() 🔝

nunique() returns a count. For category frequencies use value_counts(); for nonmissing observations use count().

print("Distinct classes:", students["CLASS1"].nunique())
fruit = pd.Series(["apple", "orange", "apple", "banana", "orange"])
print("Distinct fruits:", fruit.nunique())
print("Fruit frequencies:")
print(fruit.value_counts())
assert fruit.nunique() == 3

Expected output

Distinct classes: 3
Distinct fruits: 3
Fruit frequencies:
apple     2
orange    2
banana    1
Name: count, dtype: int64

Find values across multiple columns 🔝

Concatenate columns that represent comparable values. Distinct values across columns are different from distinct row combinations.

numbers = pd.DataFrame({"A": [1, 2, 2, 3, 4], "B": [4, 4, 3, 3, 2]})
combined = pd.concat([numbers["A"], numbers["B"]], ignore_index=True)
print(combined.unique())
assert combined.unique().tolist() == [1, 2, 3, 4]

Expected output

[1 2 3 4]

Compare categories in two datasets 🔝

A set intersection finds shared values, but sets do not preserve appearance order. Sort text results for a predictable report.

df1 = pd.DataFrame({"City": ["NY", "LA", "SF", "NY"]})
df2 = pd.DataFrame({"City": ["NY", "DC", "SF", "Chicago"]})
first = set(df1["City"].unique())
second = set(df2["City"].unique())
print("Shared:", sorted(first & second))
print("Only first:", sorted(first - second))
print("Only second:", sorted(second - first))
assert sorted(first & second) == ["NY", "SF"]

Expected output

Shared: ['NY', 'SF']
Only first: ['LA']
Only second: ['Chicago', 'DC']

Inspect categorical values and categories 🔝

Observed unique values follow appearance order. The category definition can also include unused categories; it is not the same as the observed result. Choose categorical types based on data and measurement, rather than assuming every conversion saves memory.

category = pd.Series(pd.Categorical([2, 1, 2, 3], categories=[1, 2, 3, 4]))
observed = category.unique()
print("Observed:", observed.tolist())
print("Defined categories:", category.cat.categories.tolist())
print("Distinct observed:", category.nunique())
assert observed.tolist() == [2, 1, 3]
assert category.nunique() == 3

Expected output

Observed: [2, 1, 3]
Defined categories: [1, 2, 3, 4]
Distinct observed: 3

Decide whether missing values count 🔝

unique() includes missing values. nunique() excludes them by default; use dropna=False to include them. An explicit string dtype gives this example a consistent missing-value representation. Blank strings are ordinary values until cleaned.

labels = pd.Series(["North", None, "North", "South", pd.NA, ""], dtype="string")
print("Unique:", labels.unique().tolist())
print("Known distinct:", labels.nunique())
print("Including missing:", labels.nunique(dropna=False))
print("Missing rows:", labels.isna().sum())
assert labels.nunique() == 3
assert labels.nunique(dropna=False) == 4

Expected output

Unique: ['North', <NA>, 'South', '']
Known distinct: 3
Including missing: 4
Missing rows: 2

Count distinct values by column or row 🔝

DataFrame.nunique() defaults to axis=0. Row-wise counting is useful only when the fields are comparable; avoid combining unrelated identifiers, labels and amounts into one interpretation.

print("By student column:")
print(students.nunique())
print("By row across A and B:")
print(numbers.nunique(axis=1))
assert numbers.nunique(axis=1).tolist() == [2, 2, 2, 1, 2]

Expected output

By student column:
NAME      6
ID        6
GAME      3
CLASS1    3
dtype: int64
By row across A and B:
0    2
1    2
2    2
3    1
4    2
dtype: int64

Find distinct combinations of columns 🔝

Use drop_duplicates() for distinct rows or column combinations. Counting each column separately cannot give the number of distinct pairs.

pairs = students[["CLASS1", "GAME"]].drop_duplicates()
print(pairs)
print("Distinct class/game pairs:", len(pairs))
assert len(pairs) == 5

Expected output

  CLASS1     GAME
0   Four  CRICKET
1  Three   TENNIS
2  Three  CRICKET
3   Four   HOCKEY
4   Five  CRICKET
Distinct class/game pairs: 5

Sort distinct values when required 🔝

Sort known values explicitly, rather than relying on unique(). For mixed types, first establish a consistent representation and a missing-value policy.

appearance = students["CLASS1"].unique().tolist()
alphabetical = sorted(students["CLASS1"].dropna().unique().tolist())
print("Appearance:", appearance)
print("Alphabetical:", alphabetical)
assert alphabetical == ["Five", "Four", "Three"]

Expected output

Appearance: ['Four', 'Three', 'Five']
Alphabetical: ['Five', 'Four', 'Three']

Practical example: inspect customer identifiers 🔝

This synthetic sales table has repeated customers, inconsistent whitespace and case, and missing identifiers. Count distinct customers only after defining an identifier policy. Here IDs are case-insensitive; in another system case may matter. Explicit string dtype preserves leading zeros.

raw = pd.DataFrame({
    "order_id": [101, 102, 103, 104, 105, 106, 107],
    "region": ["North", "North", "South", "South", "North", "South", "North"],
    "customer_id": [" c001 ", "C001", "c002", "C003", None, " ", "c002"]
})
raw["customer_id"] = raw["customer_id"].astype("string")
print(raw)
print("Raw known distinct:", raw["customer_id"].nunique())
assert raw["customer_id"].nunique() == 5

Expected output

   order_id region customer_id
0       101  North       c001 
1       102  North        C001
2       103  South        c002
3       104  South        C003
4       105  North        <NA>
5       106  South            
6       107  North        c002
Raw known distinct: 5

Normalize identifiers and retain review rows 🔝

Trim whitespace, standardize case and turn empty identifiers into missing values. Keep the original table for review. Continue with the Data Cleaning hub and text case operations.

cleaned = raw.copy()
ids = cleaned["customer_id"].str.strip().str.upper()
cleaned["customer_id"] = ids.mask(ids.eq(""))
review = raw.loc[cleaned["customer_id"].isna()].copy()
print("Clean known IDs:", cleaned["customer_id"].dropna().unique().tolist())
print("Known customers:", cleaned["customer_id"].nunique())
print("Review order IDs:", review["order_id"].tolist())
assert cleaned["customer_id"].nunique() == 3
assert review["order_id"].tolist() == [105, 106]

Expected output

Clean known IDs: ['C001', 'C002', 'C003']
Known customers: 3
Review order IDs: [105, 106]

Count distinct customers by region 🔝

Use groupby() with nunique(). A customer can occur in multiple regions, so adding regional counts need not equal the overall customer count. Missing IDs are not counted as customers.

report = cleaned.groupby("region").agg(
    orders=("order_id", "size"),
    known_customer_orders=("customer_id", "count"),
    distinct_customers=("customer_id", "nunique")
)
print(report)
print("Regional distinct sum:", report["distinct_customers"].sum())
print("Overall known customers:", cleaned["customer_id"].nunique())
assert report["distinct_customers"].tolist() == [2, 2]
assert report["orders"].sum() == len(raw)

Expected output

        orders  known_customer_orders  distinct_customers
region                                                   
North        4                      3                   2
South        3                      2                   2
Regional distinct sum: 4
Overall known customers: 3

Export a customer list and summary 🔝

Build a distinct known customer table for a dropdown or report. This removes duplicate identifiers from the list, not orders from the sales table. See Input and Output for file options.

customers = cleaned[["customer_id"]].dropna().drop_duplicates().sort_values("customer_id")
print(customers.to_csv(index=False).rstrip())
print(report.to_csv().rstrip())
assert len(customers) == cleaned["customer_id"].nunique()
assert len(cleaned) == len(raw)
# Optional Colab export:
# customers.to_csv("known_customers.csv", index=False)

Expected output

customer_id
C001
C002
C003
region,orders,known_customer_orders,distinct_customers
North,4,3,2
South,3,2,2

Common questions 🔝

Does unique() count occurrences? No; use value_counts(). Why are values unsorted? Appearance order is preserved. Why does a DataFrame unique() call fail? Select a Series or use drop_duplicates() for row combinations. Is a blank value missing? Not automatically. Why are regional totals higher than the overall count? The same customer can belong to more than one region. Does dropna=False count missing rows? It includes a missing category in the distinct count; use isna().sum() to count missing rows. Continue with cut(), DataFrames and NumPy arrays.

Exercise: count normalized product names 🔝

For Pen, pen, spaced PEN, Book, an empty string and a missing value, count distinct known products after trimming whitespace and ignoring case. Predict both the distinct count and the missing row count.

Exercise solution 🔝

There are two known products and two missing rows after normalization.

products = pd.Series(["Pen", "pen", " PEN ", "Book", "", None], dtype="string")
normalized = products.str.strip().str.casefold()
normalized = normalized.mask(normalized.eq(""))
print("Products:", normalized.dropna().unique().tolist())
print("Known distinct:", normalized.nunique())
print("Missing rows:", normalized.isna().sum())
assert normalized.nunique() == 2
assert normalized.isna().sum() == 2

Expected output

Products: ['pen', 'book']
Known distinct: 2
Missing rows: 2

Practice in Google Colab 🔝

Open in Google Colab View on GitHub
Run the examples in order, change customer IDs and regions, then compare raw, normalized and regional distinct counts. 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 Series.unique 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