Pandas cut(): Numeric Bands and Boundary Checks

Use pd.cut() to assign numeric values to bands. Choose explicit edges for stable reporting rules and test the boundary values before summarising the categories.

Choose bin edges for a one-dimensional input 🔝

Call the top-level function pd.cut() with a one-dimensional input. It assigns values to intervals; it does not sort the source rows. Explicit edges express fixed reporting rules. An integer bins value creates equal-width intervals from the input range; those boundaries can change with new data. qcut() instead uses quantiles to seek approximately equal-sized groups.

Categorise student marks with explicit edges 🔝

The intervals are (1, 50], (50, 70] and (70, 100]. The square bracket includes the boundary; the round bracket excludes it. The synthetic student table includes marks on a boundary, such as 70, so their placement can be checked directly.

import pandas as pd
import numpy as np
def student_data():
    return pd.DataFrame({"NAME": ["Ravi", "Raju", "Alex", "Ron", "King", "Jack"], "ID": [1, 2, 3, 4, 5, 6], "MATH": [80, 40, 70, 70, 60, 30], "ENGLISH": [80, 70, 40, 50, 60, 30]})
df = student_data()
df["my_cut"] = pd.cut(df["MATH"], bins=[1, 50, 70, 100])
print(df)
print("dtype:", df["my_cut"].dtype)
assert str(df["my_cut"].dtype) == "category"
assert df.loc[2, "my_cut"].right == 70

Expected output

   NAME  ID  MATH  ENGLISH     my_cut
0  Ravi   1    80       80  (70, 100]
1  Raju   2    40       70    (1, 50]
2  Alex   3    70       40   (50, 70]
3   Ron   4    70       50   (50, 70]
4  King   5    60       60   (50, 70]
5  Jack   6    30       30    (1, 50]
dtype: category

Create five equal-width bins and inspect edges 🔝

retbins=True returns the numeric edges as well as the categories. Integer bins include the observed extremes through a small range adjustment. Inspect the actual returned edges rather than inferring them from formatted interval labels.

df = student_data()
df["my_cut"], edges = pd.cut(df["MATH"], bins=5, retbins=True)
print(df)
print("Edges:", edges)
assert len(edges) == 6
assert df["my_cut"].notna().all()

Expected output

   NAME  ID  MATH  ENGLISH         my_cut
0  Ravi   1    80       80   (70.0, 80.0]
1  Raju   2    40       70  (29.95, 40.0]
2  Alex   3    70       40   (60.0, 70.0]
3   Ron   4    70       50   (60.0, 70.0]
4  King   5    60       60   (50.0, 60.0]
5  Jack   6    30       30  (29.95, 40.0]
Edges: [29.95 40.   50.   60.   70.   80.  ]

Use a sequence for non-uniform widths 🔝

A sequence specifies every edge. It does not create gaps: the adjacent edges below create seven contiguous bins, including narrow intervals around selected marks.

df = student_data()
df["my_cut"] = pd.cut(df["MATH"], bins=[1, 49, 50, 69, 70, 79, 80, 100])
print(df)
assert len(df["my_cut"].cat.categories) == 7
assert df.loc[0, "my_cut"] == pd.Interval(79, 80, closed="right")

Expected output

   NAME  ID  MATH  ENGLISH    my_cut
0  Ravi   1    80       80  (79, 80]
1  Raju   2    40       70   (1, 49]
2  Alex   3    70       40  (69, 70]
3   Ron   4    70       50  (69, 70]
4  King   5    60       60  (50, 69]
5  Jack   6    30       30   (1, 49]

Use a real IntervalIndex for exact intervals and gaps 🔝

An IntervalIndex can represent non-overlapping intervals with gaps. Its interval closure controls membership; right and labels do not override it. Values inside gaps become missing in the result.

intervals = pd.IntervalIndex.from_tuples([(0, 50), (70, 100)], closed="right")
values = pd.Series([30, 60, 70, 80])
bands = pd.cut(values, bins=intervals)
print(bands)
assert bands.isna().tolist() == [False, True, True, False]

Expected output

0      (0.0, 50.0]
1              NaN
2              NaN
3    (70.0, 100.0]
dtype: category
Categories (2, interval[int64, right]): [(0, 50] < (70, 100]]

Compare right=True and right=False 🔝

By default 70 belongs to (50, 70]. With right=False it belongs to [70, 100). A value of 100 is then excluded by the final interval. Always test the exact boundary values in your reporting rule.

values = pd.Series([1, 50, 70, 100])
right_closed = pd.cut(values, [1, 50, 70, 100], right=True)
left_closed = pd.cut(values, [1, 50, 70, 100], right=False)
print(pd.DataFrame({"value": values, "right_closed": right_closed, "left_closed": left_closed}))
assert pd.isna(right_closed.iloc[0])
assert right_closed.iloc[2] == pd.Interval(50, 70, closed="right")
assert left_closed.iloc[2] == pd.Interval(70, 100, closed="left")
assert pd.isna(left_closed.iloc[3])

Expected output

   value   right_closed    left_closed
0      1            NaN    [1.0, 50.0)
1     50    (1.0, 50.0]   [50.0, 70.0)
2     70   (50.0, 70.0]  [70.0, 100.0)
3    100  (70.0, 100.0]            NaN

Assign readable ordered labels 🔝

Provide one label per resulting interval. These grade labels are sample rules, not a grading standard. Ordered categories preserve the intended band order; see dtypes for type inspection.

df = student_data()
df["grade"] = pd.cut(df["MATH"], [1, 50, 75, 100], labels=["Fail", "Second", "First"])
print(df)
print("Ordered:", df["grade"].cat.ordered)
assert df["grade"].tolist() == ["First", "Fail", "Second", "Second", "Second", "Fail"]

Expected output

   NAME  ID  MATH  ENGLISH   grade
0  Ravi   1    80       80   First
1  Raju   2    40       70    Fail
2  Alex   3    70       40  Second
3   Ron   4    70       50  Second
4  King   5    60       60  Second
5  Jack   6    30       30    Fail
Ordered: True

Categorise the sum of two columns 🔝

Calculate totals explicitly before binning. If either mark can be missing, define a policy; sum() with min_count=2 can require both values.

df = student_data()
df["total"] = df["MATH"] + df["ENGLISH"]
df["grade"] = pd.cut(df["total"], [1, 100, 150, 200], labels=["Fail", "Second", "First"])
print(df)
assert df["total"].tolist() == [160, 110, 110, 120, 120, 60]
assert df["grade"].tolist() == ["First", "Second", "Second", "Second", "Second", "Fail"]

Expected output

   NAME  ID  MATH  ENGLISH  total   grade
0  Ravi   1    80       80    160   First
1  Raju   2    40       70    110  Second
2  Alex   3    70       40    110  Second
3   Ron   4    70       50    120  Second
4  King   5    60       60    120  Second
5  Jack   6    30       30     60    Fail

Include the lowest edge deliberately 🔝

include_lowest=True includes the first lower boundary when using ordinary edges. Displayed intervals may show a small precision adjustment; use membership tests and the returned edges to verify the rule.

df = student_data()
excluded = pd.cut(df["MATH"], [30, 60, 80, 100], include_lowest=False)
included = pd.cut(df["MATH"], [30, 60, 80, 100], include_lowest=True)
print(pd.DataFrame({"mark": df["MATH"], "excluded": excluded, "included": included}))
assert pd.isna(excluded.iloc[5])
assert pd.notna(included.iloc[5])

Expected output

   mark      excluded        included
0    80  (60.0, 80.0]    (60.0, 80.0]
1    40  (30.0, 60.0]  (29.999, 60.0]
2    70  (60.0, 80.0]    (60.0, 80.0]
3    70  (60.0, 80.0]    (60.0, 80.0]
4    60  (30.0, 60.0]  (29.999, 60.0]
5    30           NaN  (29.999, 60.0]

Handle duplicate edges and label counts 🔝

Duplicate edges raise ValueError by default. duplicates="drop" removes repeated edges and changes the number of intervals. Recheck your labels after dropping duplicates. The lower edge 40 is excluded by default in this example.

df = student_data()
try:
    pd.cut(df["MATH"], [40, 50, 50, 100], duplicates="raise")
except ValueError:
    print("Repeated edges require a decision.")
bands, edges = pd.cut(df["MATH"], [40, 50, 50, 100], duplicates="drop", retbins=True)
print(bands)
print("Unique edges:", edges)
assert edges.tolist() == [40, 50, 100]
assert bands.isna().sum() == 2

Expected output

Repeated edges require a decision.
0    (50.0, 100.0]
1              NaN
2    (50.0, 100.0]
3    (50.0, 100.0]
4    (50.0, 100.0]
5              NaN
Name: MATH, dtype: category
Categories (2, interval[int64, right]): [(40, 50] < (50, 100]]
Unique edges: [ 40  50 100]

Return bin numbers and reuse learned edges 🔝

labels=False returns bin indicators rather than categorical labels. With missing results, the indicators may be floating point. Save training edges when bins must remain comparable across extracts; new out-of-range values need review.

training = pd.Series([10, 20, 30, 40])
codes, edges = pd.cut(training, bins=3, labels=False, retbins=True)
new_values = pd.Series([15, 35, 50])
new_codes = pd.cut(new_values, bins=edges, labels=False)
print("Training codes:", codes.tolist())
print("New codes:", new_codes.tolist())
assert new_codes.iloc[0] == 0
assert new_codes.iloc[1] == 2
assert pd.isna(new_codes.iloc[2])

Expected output

Training codes: [0, 0, 1, 2]
New codes: [0.0, 2.0, nan]

Build sales bands after validating the amounts 🔝

For this synthetic report, accepted amounts are 0 through 1000 inclusive. Bands are Low: [0, 50], Medium: (50, 200] and High: (200, 1000]. A missing category can mean an invalid source value or an out-of-range amount; inspect the source before filling it.

raw = pd.DataFrame({"order_id": [101, 102, 103, 104, 105, 106, 107], "amount": ["0", "50", "200", "450", "-5", "bad", "1200"]})
amount = pd.to_numeric(raw["amount"], errors="coerce")
report = raw.copy()
report["amount"] = amount
report["band"] = pd.cut(amount, [0, 50, 200, 1000], labels=["Low", "Medium", "High"], include_lowest=True)
accepted = report.loc[report["band"].notna()].copy()
review = raw.loc[report["band"].isna()].copy()
print("Accepted:")
print(accepted)
print("Review original amounts:")
print(review)
assert accepted["order_id"].tolist() == [101, 102, 103, 104]
assert accepted["band"].tolist() == ["Low", "Low", "Medium", "High"]
assert review["order_id"].tolist() == [105, 106, 107]

Expected output

Accepted:
   order_id  amount    band
0       101     0.0     Low
1       102    50.0     Low
2       103   200.0  Medium
3       104   450.0    High
Review original amounts:
   order_id amount
4       105     -5
5       106    bad
6       107   1200

Summarise counts and revenue in category order 🔝

Choose observed explicitly in groupby(). observed=False includes defined categories even if empty. A row count and an amount sum answer different questions; audit review rows separately.

summary = accepted.groupby("band", observed=False).agg(orders=("order_id", "size"), revenue=("amount", "sum"))
print(summary)
print("Review rows:", len(review))
print(summary.to_csv().rstrip())
assert summary["orders"].tolist() == [2, 1, 1]
assert summary["revenue"].tolist() == [50, 200, 450]
# Optional Colab export:
# summary.to_csv("sales_band_summary.csv")

Expected output

        orders  revenue
band                   
Low          2     50.0
Medium       1    200.0
High         1    450.0
Review rows: 3
band,orders,revenue
Low,2,50.0
Medium,1,200.0
High,1,450.0

Common questions 🔝

Why is the minimum missing? Check include_lowest and interval closure. Why is the maximum missing? With right=False the final right edge is excluded. Why do bands change? Integer bins use the current range; save edges or choose fixed business boundaries. Does precision round my source data? No; do not infer membership from formatted labels. Why are values missing inside the range? Check IntervalIndex gaps and source missing values. Should I use cut() or qcut()? Choose fixed value intervals versus quantile-based grouping according to the report. Continue with value_counts() and the student grading exercise.

Exercise: test every age boundary 🔝

Group ages 0, 17, 18, 64 and 65 into Child: [0, 18), Adult: [18, 65) and Senior: [65, infinity). Use right=False and predict the labels before running the solution.

Exercise solution 🔝

Explicit left-closed intervals include 18 in Adult and 65 in Senior.

ages = pd.Series([0, 17, 18, 64, 65])
answer = pd.cut(ages, [0, 18, 65, np.inf], right=False, labels=["Child", "Adult", "Senior"])
print(answer)
assert answer.tolist() == ["Child", "Child", "Adult", "Adult", "Senior"]

Expected output

0     Child
1     Child
2     Adult
3     Adult
4    Senior
dtype: category
Categories (3, str): ['Child' < 'Adult' < 'Senior']

Practice in Google Colab 🔝

Open in Google Colab View on GitHub
Run the examples in order, change bin edges and boundary values, then compare band assignments and summaries. 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 cut documentation.




Subscribe to our YouTube Channel here



plus2net.com







Python Video Tutorials
Python SQLite Video Tutorials
Python MySQL Video Tutorials
Python Tkinter Video Tutorials
✖
We use cookies to improve your browsing experience. . Learn more
HTML MySQL PHP JavaScript ASP Photoshop Articles Contact us
© 2000-2026 plus2net.com All rights reserved worldwide Privacy Policy Disclaimer