Binning and Discretization
Binning (also called discretization) converts a continuous numeric variable into a smaller number of discrete categories, or bins. This is useful for building histograms, simplifying a model's feature space, creating human-readable groups (like age brackets or income tiers), and reducing the effect of noise or outliers in a continuous measurement. Pandas provides two complementary functions for this: cut, which bins values into intervals you define (by fixed-width or explicit edges), and qcut, which bins values into groups containing roughly equal numbers of observations based on quantiles.
Cricket analogy: Converting continuous strike rates into readable tiers like 'anchor', 'accelerator', 'finisher' is binning, and pandas offers cut for fixed-width run brackets versus qcut for grouping batters into equal-sized quartiles by performance.
pd.cut: equal-width or custom-edge bins
pd.cut(x, bins) partitions the data by value, not by count. If bins is an integer n, pandas divides the range from the minimum to maximum value into n equal-width intervals. If bins is a list of numbers, those become the explicit edges, giving you full control over bucket boundaries (e.g. defining age groups as [0, 18, 35, 60, 100]). By default the intervals are right-closed and left-open — (18, 35] — meaning a value exactly on the left edge falls into the previous bin unless right=False is passed. The labels parameter lets you replace the default interval notation with meaningful category names.
Cricket analogy: pd.cut(ages, [0, 18, 35, 60, 100]) splits a squad into junior, prime, veteran, and legend age brackets using explicit edges, and by default a 35-year-old lands in (18, 35] since the interval is right-closed, with labels renaming these to readable names.
import pandas as pd
ages = pd.Series([4, 17, 18, 35, 42, 67, 90])
bins = [0, 18, 35, 60, 100]
labels = ['Child', 'Young Adult', 'Adult', 'Senior']
groups = pd.cut(ages, bins=bins, labels=labels)
print(groups.tolist())
# ['Child', 'Child', 'Young Adult', 'Young Adult', 'Adult', 'Senior', 'Senior']
# Note: age 18 falls in 'Child' bin because default intervals are (0, 18], not [18, 35)pd.qcut: quantile-based, equal-frequency bins
pd.qcut(x, q) instead divides data so each resulting bin contains approximately the same number of observations, regardless of the width of the value range each bin spans. Passing q=4 creates quartiles, q=10 creates deciles, or you can pass explicit quantile boundaries like [0, 0.25, 0.5, 0.75, 1.0]. qcut is the natural choice when you want balanced group sizes for analysis (e.g. splitting customers into 'low', 'medium', 'high' spenders by tercile) rather than balanced value ranges, which is what cut provides. With heavily skewed data or many duplicate values, qcut can raise an error about non-unique bin edges unless duplicates='drop' is specified.
Cricket analogy: pd.qcut(strike_rates, 4) splits batters into quartiles with roughly equal squad counts in each tier regardless of the value spread, useful for labeling 'low', 'medium', 'high' scorers by tercile, though many tied strike rates can trigger a non-unique bin edge error unless duplicates='drop'.
import pandas as pd
spend = pd.Series([12, 15, 20, 22, 25, 40, 41, 42, 100, 250])
quartiles = pd.qcut(spend, q=4, labels=['Q1', 'Q2', 'Q3', 'Q4'])
print(quartiles.tolist())
# ['Q1', 'Q1', 'Q1', 'Q2', 'Q2', 'Q3', 'Q3', 'Q3', 'Q4', 'Q4']
# Each quartile contains roughly 2-3 of the 10 values, not equal value rangescut and qcut both return a pandas Categorical (or an ordered Series of Interval objects if no labels are given), which integrates directly with groupby — you can group a DataFrame by cut(df['age'], bins) to compute per-bin aggregates in one step.
A frequent mistake is assuming cut produces equal-sized groups the way qcut does — cut only guarantees equal-width intervals, so with skewed data some bins can end up nearly empty while others absorb most of the observations.
- pd.cut bins values by fixed value ranges (equal-width by default, or custom edges you supply).
- pd.qcut bins values by quantiles so each bin has roughly equal observation counts.
- Default interval notation is right-closed, left-open — e.g. (18, 35] — controllable via the right parameter.
- The labels parameter replaces raw interval notation with human-readable category names.
- qcut can fail on data with many duplicate values unless duplicates='drop' is passed.
- The output of cut/qcut is a Categorical that works directly with groupby for per-bin aggregation.
Practice what you learned
1. What is the primary difference between pd.cut and pd.qcut?
2. With bins=[0, 18, 35, 60, 100] and default settings, which bin does a value of exactly 18 fall into?
3. What does pd.qcut(series, q=4) produce?
4. Why might pd.qcut raise an error on a dataset with many repeated values?
5. What type of object does pd.cut return when a labels argument is not provided?
Was this page helpful?
You May Also Like
Sorting and Ranking
How to order DataFrames and Series with sort_values and sort_index, and how to assign competitive ranks to values with the rank method and its tie-breaking strategies.
Handling Outliers
Learn common statistical techniques — z-score, IQR, and percentile capping — for detecting and treating outliers in pandas and NumPy data.
GroupBy Basics
Understand pandas' split-apply-combine model via groupby(), including grouping keys, selecting columns, and iterating over groups.
Aggregation Functions
Explore the built-in and custom aggregation functions available after groupby, including agg() for applying multiple statistics at once.