Knowledge base / Python

pandas nunique: distinct counts, axis and missing values

I separate row counts, known values and distinct customers. Check correct axis=1 results and the two independent missing-data decisions in grouping.

Kajo RudzińskiPublished Updated 5 min read

A pixel-art desk with trays of repeated shapes and stacks counting their variety

How many different customer identifiers appear in a table? That differs from asking how many rows it contains. One customer can place several orders, and some records may have no identifier. nunique counts distinct values, but you still need to decide how to treat missing entries and which direction to count across.

I will demonstrate Series.nunique() and DataFrame.nunique() using synthetic data. There is no top-level pd.nunique() function. I executed the examples with Python 3.14.4, pandas 3.0.6 and 3.1.0, and NumPy 2.5.4, without PyArrow. Run the blocks in sequence in one script or notebook.

A Series count and a missing-value decision

Start with one column. nunique() returns the number of distinct values, excluding missing entries by default. Setting dropna=False includes the missing marker as a distinct item in the Int64 Series below. The capital I identifies a pandas integer dtype that supports pd.NA, rather than an ordinary NumPy integer dtype.

# example: series
import pandas as pd

customers = pd.Series([10, 10, 20, pd.NA, 20, pd.NA], dtype="Int64")
print(len(customers))
print(customers.count())
print(customers.nunique())
print(customers.nunique(dropna=False))
6
4
2
3

There are six entries, four known values, two distinct known customers and three distinct items when the missing marker is included. None of those counts substitutes for the others. Name exactly what you calculated when documenting a report. “Customer count” without a rule for empty identifiers leaves room for a misleading interpretation.

In particular, two missing identifiers do not establish two known people or one identified person. Counting the missing marker is a technical operation. Counting actual customers requires identity information that those records do not provide. The Series.nunique documentation describes the counting rule itself, without making that business decision for you.

A DataFrame returns a count for each column

The default axis=0 counts distinct values separately in each column. The result is a Series indexed by column names. I convert it to a dictionary below to keep the output compact and easy to compare. Column C combines numbers and missing entries, so pandas represents it with a floating-point dtype.

# example: columns
frame = pd.DataFrame({
    "A": [1, 2, 2, 3, 3, 3],
    "B": ["a", "b", "a", "b", "b", "b"],
    "C": [None, 1, 2, 2, 2, None],
})
print(frame.nunique().to_dict())
print(frame.nunique(dropna=False).to_dict())
{'A': 3, 'B': 2, 'C': 2}
{'A': 3, 'B': 2, 'C': 3}

A contains 1, 2 and 3. B has two labels. C has two known numbers, with its count rising to three when the missing marker is included. I do not add these results to obtain the number of distinct rows. Distinct values within separate columns and distinct combinations of those columns describe different properties of a table.

Axis 1 compares values within each row

With axis=1, each result describes one row. Here we compare the values in A, B and C. Notice the row at index 2: numeric values 2 and 2.0 compare equal, while the text a is a second distinct item. Its count is therefore two, rather than three.

# example: rows
print(frame.nunique(axis=1).tolist())
print(frame.nunique(axis=1, dropna=False).tolist())
[2, 3, 2, 3, 3, 2]
[3, 3, 2, 3, 3, 3]

These are the corrected results for the exact dataset shown. The DataFrame.nunique documentation distinguishes the two counting directions. A row-wise count can be useful when fields are comparable, such as three answers to the same question. Comparing age, product code and amount merely because they share a row rarely creates a useful measure.

Do not confuse this with detecting repeated rows. axis=1 does not ask whether row 2 is a copy of row 3. It measures variety inside each individual observation. Other methods handle duplicate records, where the comparison takes place between rows using one or more selected fields as the key.

A pixel-art matrix with separate counting results along its columns and rows

Grouping has a separate missing-key rule

Suppose I need customer counts by region. Passing dropna to groupby determines whether to retain a group with a missing region. Passing dropna to nunique determines whether to count a missing customer identifier inside each group. These are separate decisions even though the parameter has the same name in both places.

# example: groups
orders = pd.DataFrame({
    "region": pd.Series(["north", "north", "south", "south", pd.NA, pd.NA], dtype="string"),
    "customer_id": pd.Series([1, 2, 2, pd.NA, 3, pd.NA], dtype="Int64"),
})
grouped = orders.groupby("region", dropna=False)["customer_id"]
print(grouped.nunique().index.tolist())
print(grouped.nunique().tolist())
print(grouped.nunique(dropna=False).tolist())
['north', 'south', <NA>]
[2, 1, 1]
[2, 2, 2]

The southern region has one known customer and another observation without an identifier. The missing-region group has a similar situation. Keeping the default omission of missing grouping keys would remove that final group. During a quality check I want to see it, even if a later business report presents unknown regions in a separate section.

A count does not show frequency or every error

nunique answers “how many different values,” while unique shows which values they are. value_counts provides their frequencies. Two columns can each contain two different labels even if one is evenly balanced and the other is dominated by one label. A distinct count alone cannot reveal that distribution.

When checking a table key, I inspect missing values and repetition too. Equality between the number of distinct known values and the number of rows is useful evidence for one column, but it does not establish the identifier’s business meaning. Nor does it prove that identifiers in two systems refer to the same people. Composite keys require checking combinations of fields.

Exercise: all missing versus completely empty

Before using the count in an automated report, I check what happens without known observations. A Series containing two missing entries and a Series containing no entries both produce zero by default. Including missing values makes their answers differ. This helps distinguish “the values are unknown” from “no records were received.”

# example: boundaries
missing = pd.Series([pd.NA, pd.NA], dtype="Int64")
empty = pd.Series([], dtype="Int64")
print(missing.nunique(), missing.nunique(dropna=False))
print(empty.nunique(), empty.nunique(dropna=False))
0 1
0 0

Now add a known value to each Series and predict both counts. Also consider whether an empty string in a text column should count as missing. Pandas cannot choose that business rule for you. Inspect actual labels with unique, and use drop_duplicates when the task is to select representative rows.

Key takeaways

Does pandas.nunique() exist?

There is no top-level pd.nunique function. Use Series.nunique or DataFrame.nunique; appropriate groupby objects also provide nunique.

What does axis=1 do in nunique?

It counts distinct values within each row. It does not count distinct table rows or check whether one record duplicates another.

Does nunique count missing values?

It excludes them by default. dropna=False includes them in the homogeneous Series shown here. The separate dropna argument in groupby controls missing grouping keys.

Kajo Rudziński

Newsletter KajoData

Let’s keep in touch.

I write about data analysis, learning and changing careers. My newsletter and the sign-up form are in Polish. If that works for you, I’d be glad to see you there.