Using pandas Categorical to Reduce Memory and Speed Up groupby
Learn how converting low‑cardinality string columns to pandas Categorical reduces memory and accelerates groupby operations, with a concrete example and practical verification steps.
10 Dec 2025, 08:50 UTC

Why object columns can become a bottleneck
When a DataFrame stores text data as the default object dtype, each unique string is kept as a separate Python object. For columns that contain only a handful of distinct values (e.g., department names, education levels, or survey responses), this representation wastes memory and makes groupby operations slower because pandas has to hash and compare full strings for every row.
Thesis: low‑cardinality strings benefit from Categorical dtype
Converting such columns to category stores the distinct values once in an internal lookup table and represents each row by an integer code. This reduces the memory footprint and allows pandas to perform aggregations on the compact codes, which is typically faster.
When the conversion helps
- The column has relatively few unique values compared to the number of rows (low cardinality).
- You plan to perform grouping, sorting, or value‑counts on that column.
- Memory usage is a concern (e.g., working with large CSV files or limited RAM).
When to avoid it
- The column is high‑cardinality (many unique values, such as IDs or free‑form text). The overhead of the category table can outweigh any savings.
- You frequently need to add new categories after the conversion; mismatched categories can produce unexpected
NaNvalues during merges or concatenations.
Worked example: survey dataset
Suppose you have a CSV file survey.csv with columns employee_id, department, education, and salary. The department and education fields contain only a few dozen distinct values each.
import pandas as pd
# Load the data (adjust the path as needed)
df = pd.read_csv('survey.csv')
# Inspect original dtypes
print(df.dtypes)
# Convert low‑cardinality columns to Categorical
df['department'] = df['department'].astype('category')
df['education'] = df['education'].astype('category')
# Verify the conversion
print(df[['department', 'education']].dtypes)
# Measure memory usage (deep=True accounts for the actual string storage)
print('Memory usage before conversion (approx):', df.memory_usage(deep=True).sum())
print('Memory usage after conversion (approx):', df.memory_usage(deep=True).sum())
# Example groupby operation to compare speed
# You can time this with %timeit in a notebook or with time.perf_counter() in a script
result = df.groupby('department').salary.mean()
print(result)
After running the script, you should see the dtype of the two columns change from object to category. The memory_usage(deep=True) call will show a lower total after conversion, reflecting the reduced storage of the string values. Repeating the groupby aggregation and timing it (e.g., with %timeit in Jupyter) will typically reveal a shorter execution time because the grouping is performed on integer codes rather than on the full strings.
Practical checks and limitations
- Verify the conversion: Check
df['column_name'].dtype; it should readcategory. - Measure memory: Use
df.memory_usage(deep=True).sum()before and after theastype('category')step. - Time a groupby: In a notebook, run
%timeit df.groupby('department').salary.mean(); in a script, wrap the call withtime.perf_counter(). - High‑cardinality warning: If you apply
astype('category')to a column with many unique values (e.g., a user‑ID column), the memory usage may not drop and could even increase due to the extra categories table. - Merging caution: When concatenating DataFrames that have categorical columns, ensure the categories match; otherwise, pandas will align them and produce
NaNfor mismatched levels. After concatenation, you can reset categories withdf['column'] = df['column'].astype('category').
Actionable takeaway
For any string column where the number of distinct values is small relative to the row count, converting to pandas.Categorical is a low‑effort way to cut memory consumption and speed up common analytical operations. Start by profiling memory with memory_usage(deep=True), apply the conversion, then confirm the impact on both storage and the runtime of your typical groupby or value_counts workflow. If you notice no benefit or a slowdown, revert the column to object and reconsider the cardinality of that column.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.