Skip to main content
v2026.11,858 entries · CC-BY 4.0
LAC HealthMedical & Research SuppliesReagents, PPE & instruments — chain-of-custody documented.Fast, traceable sourcing built for regulated research environments, from bench consumables to instrumentation.Shop lac.us CodeCASRAIlac.us

pandas cut: pd.cut() Syntax, Bins, Labels and Edge Cases

How pandas.cut bins a continuous variable: bins, labels, right and include_lowest, why edge values become NaN, and when qcut fits better, with run output.

Written and maintained by CASRAI Editorial Board

Last updated

pandas.cut() sorts the values of a one-dimensional array or Series into discrete intervals (bins), turning a continuous variable such as age into a categorical one. You pass the data and either a number of equal-width bins or your own bin edges, and optionally labels to name the groups. The pitfalls are at the edges: with the defaults, a value equal to the first edge, or any value beyond the last edge, comes back as NaN. This guide shows each case with run output. Documentation version: pandas 3.1.1, current as of 2026-11-07; example output was produced with pandas 3.0.5 and NumPy 2.5.1.

What it is used for in research

Researchers use pd.cut() to turn a continuous measurement into ordered groups. The pandas documentation’s own example is converting ages to age ranges. Typical uses are age bands for a baseline table, score or concentration bands defined in a protocol, and bins for a frequency count. The function does not choose cut-points for you: an integer gives equal-width bins, and a list gives exactly the edges you supply.

Grouping a continuous variable discards detail, and the result is an ordered categorical variable (see levels of measurement and categorical variable examples for what that changes). Altman and Royston discuss the cost of dichotomising continuous variables in a 2006 BMJ Practice Statistics Note listed under Sources. Whichever edges you use, report them and which side of each interval is closed, so the grouping can be reproduced.

Install or access

pd.cut is part of pandas. The official installation page says the pandas development team distributes pandas through conda-forge, PyPI and GitHub, and gives these commands:

pip install pandas
conda install -c conda-forge pandas

The page recommends installing and running pandas from a virtual environment, for example conda create -c conda-forge -n name_of_my_env python pandas followed by conda activate name_of_my_env. It lists NumPy as a required dependency (minimum supported version 2.0.2 for pandas 3.1.1) and notes that pandas from Anaconda is not officially managed by the pandas development team. Then import it in your script or notebook: import pandas as pd and import numpy as np.

Step-by-step: bin a column with pd.cut

The signature in the current documentation is pandas.cut(x, bins, right=True, labels=None, retbins=False, precision=3, include_lowest=False, duplicates='raise', ordered=True).

  1. Put the values in a one-dimensional Series or array. The documentation says x must be 1-dimensional; a DataFrame column such as df["age"] qualifies. To start from a DataFrame, see create a DataFrame in Python.
  2. Choose bins. An integer makes that many equal-width bins across the range of x, and the range is extended by 0.1% on each side so the minimum and maximum are included. A sequence of scalars defines the bin edges exactly, allows non-uniform widths, and does not extend the range. An IntervalIndex defines the exact bins and must be non-overlapping.
  3. Call pd.cut(x, bins). With the defaults, each bin includes its right edge: edges [1, 2, 3, 4] mean (1, 2], (2, 3], (3, 4].
  4. Name the groups with labels if you want readable categories. The labels must be the same length as the resulting bins. labels=False returns integer bin codes instead.
  5. Settle the edge convention. Use right=False to close the left edge instead, and include_lowest=True to put values equal to the first edge into the first bin.
  6. Count the NaN results with result.isna().sum() before you use the grouping, so you know how many rows fell outside the bins.
  7. Store the result as a column, for example df["age_group"] = pd.cut(df["age"], bins=edges, labels=labs).

The parameters

Parameter Default What the documentation says
x required 1-dimensional ndarray or Series to bin.
bins required int (equal-width bins, range extended by 0.1% on each side), sequence of scalars (exact edges, no extension) or IntervalIndex (exact, non-overlapping bins).
right True Whether bins include the rightmost edge. Ignored when bins is an IntervalIndex.
labels None Labels for the returned bins, same length as the resulting bins; False returns only integer indicators; True raises an error; ignored for an IntervalIndex.
retbins False Also return the bins. Useful when bins is a single number.
precision 3 Precision at which bin labels are stored and displayed.
include_lowest False Whether values equal to the first bin edge go in the first bin. No effect when right=False or bins is an IntervalIndex.
duplicates 'raise' If bin edges are not unique, raise ValueError or drop the non-unique edges ('drop').
ordered True Whether the labels are ordered. With False, labels must be provided.

Worked example: twelve illustrative ages

The ages below are an illustrative composite for demonstration, not data from a study. The youngest participant is 18 and the oldest is 70.

import pandas as pd
ages = pd.Series([18, 22, 25, 31, 34, 38, 41, 47, 52, 58, 63, 70],
                 index=[f"P{i:02d}" for i in range(1, 13)], name="age")

Equal-width bins: bins=3

groups, edges = pd.cut(ages, bins=3, retbins=True)
print(edges)
[17.948      35.33333333 52.66666667 70.        ]

The lowest edge is 17.948, just below the minimum of 18, which is the 0.1% extension the documentation describes (the documentation’s own example shows the same lower edge, 0.994 for a minimum of 1). Five participants fall in (17.948, 35.333], four in (35.333, 52.667] and three in (52.667, 70.0]. Equal-width bins land on awkward edge values such as 35.333; pass explicit edges when the cut-points must be round numbers.

Explicit edges with the defaults: two values vanish

edges = [18, 30, 45, 65]
grp = pd.cut(ages, bins=edges)
print(grp.isna().sum())
2

P01 (age 18) is NaN because the first interval is (18, 30], which excludes 18 itself, and P12 (age 70) is NaN because it is above the last edge, 65. The remaining ten participants are counted as 2 in (18, 30], 4 in (30, 45] and 4 in (45, 65]. Note that grp.value_counts() leaves NaN out by default, so the counts add up to 10 and nothing in that table warns you that two rows were dropped. This is why step 6 above checks isna() explicitly.

include_lowest=True with labels

labs = ["18-30", "31-45", "46-65"]
grp = pd.cut(ages, bins=edges, include_lowest=True, labels=labs)
print(grp.isna().sum(), grp.dtype, grp.cat.ordered)
1 category True

Now P01 is in “18-30” (counts 3, 4 and 4) and only P12 (age 70) is NaN, because include_lowest does not cover values above the last edge. Without labels, the first interval is displayed as (17.999, 30.0]: the documentation explains that the left edge of the first default label is lowered slightly, so integer bins get float labels.

right=False and a wider top edge

grp = pd.cut(ages, bins=[18, 30, 45, 65, 120], right=False)

The intervals are now [18, 30), [30, 45), [45, 65) and [65, 120), with 3, 4, 4 and 1 participants, and no NaN. Here each interval includes its left edge and excludes its right edge, so a participant aged exactly 30 would go into [30, 45), not the first bin. Choose the top edge deliberately: 120 here is only a number larger than any plausible value.

Integer codes: labels=False

codes = pd.cut(ages, bins=[17, 30, 45, 65, 120], labels=False)

This returns the codes 0 for P01 to P03, 1 for P04 to P07, 2 for P08 to P11 and 3 for P12. The codes count bins from 0, in order, regardless of the edge values.

Output and how to read it

What comes back depends on the input and on labels, as the documentation describes:

Call Returns
labels=None (default) A Series for Series input or a Categorical for other inputs, holding Interval values.
labels a sequence A Series for Series input or a Categorical for other inputs, holding your labels. With ordered=True the categorical is ordered.
labels=False A 1-d ndarray (or Series) of integers.
retbins=True The result above plus the computed bins; for scalar or sequence bins the bins are an ndarray.

Read an interval such as (30, 45] as “greater than 30 and up to and including 45”. A square bracket is closed (value included) and a round bracket is open (value excluded).

Which values become NaN

Situation Result Basis
The input value is already NA NA in the result Documentation Notes; the example below the table
The value is outside the range of the bins NA Documentation Notes; P12 (age 70) in the example
The value equals the first edge with right=True and include_lowest=False NA P01 (age 18) in the example
The value falls between two bins of a non-contiguous IntervalIndex NA Documentation Notes

For a missing input, pd.cut(pd.Series([1.0, np.nan, 5.0]), [0, 2, 6]) returns (0.0, 2.0], NaN and (2.0, 6.0]. Report how many rows were NaN after binning separately from how many were missing before it; the two have different causes. For the wider cleaning workflow, see data cleaning steps for research data.

Troubleshooting

Some rows came back NaN

Check the first and last edges against the minimum and maximum of your data. Either widen the outer edges, set include_lowest=True, or use right=False with an upper edge above the maximum, as in the worked example.

ValueError: Bin edges must be unique

Raised when two edges are equal, for example pd.cut(ages, bins=[0, 30, 30, 70]); the message in the run suggests setting the duplicates keyword. Passing duplicates="drop" removes the repeated edge. The pandas documentation’s example uses edges [0, 2, 4, 6, 10, 10] with duplicates="drop" and gets the bins [0, 2, 4, 6, 10].

ValueError: bins must increase monotonically

Raised for edges that go down, such as [0, 50, 30, 70]. Sort the edges in ascending order.

ValueError: Input array must be 1 dimensional

The documentation requires x to be 1-dimensional. Pass one column or one row at a time, for example df["age"], not a two-dimensional array or a whole DataFrame.

ValueError: Bin labels must be one fewer than the number of bin edges

The number of labels must equal the number of bins, which is the number of edges minus one. Three labels need four edges, as in the worked example. labels=True also raises an error; use a sequence of labels or False.

I want the same label on more than one bin

Pass ordered=False, which the documentation says can be used to allow non-unique labels. Its example is labels=["B", "A", "B"] with ordered=False, which gives the categories A and B.

The interval labels show too many or too few decimals

Set precision. For the values 0.12345, 0.5 and 0.98765 split into two bins, the default precision of 3 displays (0.123, 0.556] and (0.556, 0.988]; with precision=1 the same bins display as (0.1, 0.6] and (0.6, 1.0]. The label text changes, not which bin each value belongs to.

Free alternatives

These are the alternatives named in the pandas documentation. Each is part of pandas or NumPy and is installed the same way as above.

Tool Call What the documentation says
pandas.qcut pd.qcut(x, q, labels=None, retbins=False, precision=3, duplicates='raise') Quantile-based discretization: equal-sized buckets based on rank or sample quantiles. q is a number of quantiles (10 for deciles, 4 for quartiles) or a list such as [0, .25, .5, .75, 1.].
IntervalIndex bins pd.cut(x, pd.IntervalIndex.from_breaks([0, 40, 70])) Defines the exact bins and must be non-overlapping. Values outside the intervals become NaN.
NumPy bin edges pd.cut(x, bins=np.histogram_bin_edges(x, bins="auto")) The cut documentation lists numpy.histogram_bin_edges as the function that calculates only the edges of the bins used by the histogram function, and says it can be used along with cut to calculate bins according to some predefined methods.
Indicator columns pd.get_dummies(pd.cut(values, bins)) The pandas user guide notes that get_dummies is often used with discretization functions like cut().

cut or qcut: how the counts differ

Use cut when the edges mean something in your protocol (age bands, score bands). Use qcut when you want groups of about equal size. The documentation defines cut as binning by value (equal-width bins for an integer) and qcut as equal-sized buckets based on rank or sample quantiles, so on skewed data the counts diverge. Twelve illustrative reaction times in milliseconds (310, 325, 340, 350, 360, 375, 390, 420, 480, 650, 900, 1500), split into four groups each way:

Method Intervals Counts
pd.cut(rt, bins=4) (308.81, 607.5], (607.5, 905.0], (905.0, 1202.5], (1202.5, 1500.0] 9, 2, 0, 1
pd.qcut(rt, q=4) (309.999, 347.5], (347.5, 382.5], (382.5, 522.5], (522.5, 1500.0] 3, 3, 3, 3

cut leaves one bin empty and puts nine of twelve values in one bin, because the single large value stretches the range. qcut balances the counts but its edges come from the data, so they will differ between samples. For the quantile idea itself, see percentile and quantile. With many tied values qcut can also fail on non-unique edges: for the values 1, 1, 1, 1, 1, 1, 2, 3, 4, 5 with q=4, the run raised “Bin edges must be unique”, and duplicates="drop" returned two bins, (0.999, 2.75] with 7 values and (2.75, 5.0] with 3.

FAQ

What does pandas cut do?

It bins values into discrete intervals, which is how you go from a continuous variable to a categorical one. Give it an array or Series plus a bin count or bin edges; it returns the interval or label each value belongs to, or integer codes when labels=False.

What is the difference between pandas cut and qcut?

cut bins by value: an integer gives equal-width bins and a list gives your own edges. qcut bins by rank or sample quantiles, giving buckets of about equal size. See the cut or qcut comparison above for counts on skewed data.

How do I include the lowest value in pandas cut?

Set include_lowest=True. It places values equal to the first bin edge into the first bin, and has no effect when right=False or when bins is an IntervalIndex. It does not rescue values above the last edge, which stay NaN.

How do I set the bins in pandas cut?

Pass an integer for equal-width bins, a list of ascending edges for exact bins, or an IntervalIndex for exact non-overlapping bins. With an integer, use retbins=True to see the edges it computed.

How do I use pandas cut on a DataFrame column in Python?

Pass the column as x and assign the result to a new column: df["age_group"] = pd.cut(df["age"], bins=edges, labels=labs). For a Series input the result is a Series with category dtype (unless labels=False).

Is pandas clip the same as cut?

No. clip trims values at thresholds: values outside the boundary are assigned the boundary value, and the result has the same type as the caller. cut assigns each value to an interval and returns a categorical (or integer codes). For clipping, see NumPy clip, which also covers the pandas equivalent.

Sources

Current as of 2026-11-07.

Follow CASRAI

Research-administration guidance, standards updates and independent tool reviews.

Ask CASRAI · free to try

Ask about pandas cut: pd.cut() Syntax, Bins, Labels and Edge Cases

Ask your first 2 questions free below. Subscribers get 150 a day for $29 a month.

An AI assistant specialized in research administration. It cites the sources behind every answer, labels web answers and says when it can't answer.

Answers draw on CASRAI's guides and dictionary plus the federal and funder documents we index: Federal Register, Grants.gov, Regulations.gov and UKRI.

Works on this site and inside Claude, Cursor and the AI tools you already use.

Everything CASRAI publishes — this page, the dictionary, the guides and the news — stays free to read, with no account and no card.

Referenced across the research world

University of Cambridge logoColumbia University logoCrossref logoUniversity of Edinburgh logoHarvard University logoUniversity of Oxford logoPrinceton University logoStanford School of Medicine logoUniversity College London logoORCID logoUniversity of Cambridge logoColumbia University logoCrossref logoUniversity of Edinburgh logoHarvard University logoUniversity of Oxford logoPrinceton University logoStanford School of Medicine logoUniversity College London logoORCID logo
  • University of Cambridge logo
  • Columbia University logo
  • Crossref logo
  • University of Edinburgh logo
  • Harvard University logo
  • University of Oxford logo
  • Princeton University logo
  • Stanford School of Medicine logo
  • University College London logo
  • ORCID logo

View CASRAI adoption →