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.
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 Threeunique() 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: strUse 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']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() == 3Expected output
Distinct classes: 3
Distinct fruits: 3
Fruit frequencies:
apple 2
orange 2
banana 1
Name: count, dtype: int64Concatenate 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]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']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() == 3Expected output
Observed: [2, 1, 3]
Defined categories: [1, 2, 3, 4]
Distinct observed: 3unique() 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) == 4Expected output
Unique: ['North', <NA>, 'South', '']
Known distinct: 3
Including missing: 4
Missing rows: 2DataFrame.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: int64Use 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) == 5Expected output
CLASS1 GAME
0 Four CRICKET
1 Three TENNIS
2 Three CRICKET
3 Four HOCKEY
4 Five CRICKET
Distinct class/game pairs: 5Sort 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']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() == 5Expected 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: 5Trim 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]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: 3Build 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,2Does 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.
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.
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() == 2Expected output
Products: ['pen', 'book']
Known distinct: 2
Missing rows: 2Open 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.
Author & Instructor at plus2net
I write and maintain practical tutorials on Python, PHP, SQL, JavaScript, HTML, jQuery, and web development at plus2net. The tutorials focus on clear explanations, working examples, and code that readers can test and adapt while learning.