把连续数值切成若干区间(分箱、离散化)是数据预处理里最常见的一步:成绩分等级、金额分档、年龄分段、销量分层都要用它。pandas 给出了两个入口函数:pd.cut() 按数值宽度切分,pd.qcut() 按样本频率切分。下面结合一段可复现的示例数据,把两个函数的参数含义、输出形态和统计方式讲清楚。
一、准备示例数据
先构造一列 0~100 之间的整数分数,固定随机种子保证结果可复现:
- import pandas as pd
- import numpy as np
- np.random.seed(42) # 固定随机种子,结果可复现
- data = pd.Series(np.random.randint(0, 101, size=10), name="score")
- print("原始数据:")
- print(data)
复制代码
后面的所有分箱操作都复用这列 data。
二、pd.cut():按区间宽度做等宽分箱
pd.cut() 的切分依据是数值范围本身,每个箱子的宽度相同,适合分布相对均匀、业务上按数值区间划分的场景。
1. 只传整数 bins
- bins_1 = pd.cut(data, bins=5)
- print(bins_1)
复制代码
bins=5 表示把数据的最小值到最大值之间均分成 5 段等宽区间,默认标签是原始区间字符串,形如 (a, b],即左开右闭。返回值不是普通字符串,而是 category 类型,其 Categories 信息会写明区间个数与区间类型,例如 Categories (5, interval[int64, right]),并保持区间从小到大的有序关系,便于后续比较和排序。
2. 自定义边界加业务标签
当分档规则由业务确定(考试等级、会员等级、风险档位)时,直接给出边界列表比自动等分更可控:
- bins = [0, 60, 70, 80, 90, 100] # 自定义分数区间
- labels = ["不及格", "及格", "良好", "优秀", "满分"] # 与区间一一对应的标签
- bins_2 = pd.cut(data, bins=bins, labels=labels, include_lowest=True)
- print(bins_2)
复制代码
这里的三个参数各有明确作用:
bins:列表长度是 N,切出的区间个数就是 N-1,边界值本身构成每个箱子的端点,不是箱子数。
labels:按区间顺序替换默认的区间字符串,长度必须等于区间个数,否则会报错;替换后 Categories 显示的是这些自定义标签的有序集合。
include_lowest=True:默认区间是左开右闭,最小值落在第一个区间的左端点上会被排除,该参数把第一个区间改为包含左端点,避免边界值变成 NaN。
3. 统计每个箱子的样本数
- print(bins_2.value_counts())
复制代码
value_counts() 依据标签统计落入每个区间的样本数,输出直接可用于报表。由于结果是 category 类型,所有类别都会被保留,没有样本落入的区间会以 0 出现(示例数据中最高分段没有样本,统计结果里仍会列出该档并为 0),这样各档次人数一目了然,不会出现某一档凭空消失的情况。
三、pd.qcut():按样本数量做等频分箱
当数据分布很不均匀(典型的收入、销量、订单金额等长尾分布)时,等宽分箱会让大部分样本挤在一两个箱子里,另一些箱子为空。pd.qcut() 改为按分位数切分,每个箱子包含的样本数量大致相等:
- # 1. 等频分箱:分成 4 个箱子
- q_bins_1 = pd.qcut(data, q=4)
- print(q_bins_1)
- # 2. 等频分箱 + 自定义标签
- q_labels = ["低分段", "中低分段", "中高分段", "高分段"]
- q_bins_2 = pd.qcut(data, q=4, labels=q_labels)
- print(q_bins_2)
- # 统计每个频段的样本数
- print(q_bins_2.value_counts())
复制代码
q=4 表示按 25%、50%、75% 的分位点把数据切成四段,每个箱子的样本数尽可能接近。关键差异在于:分箱边界不再由数值范围决定,而是由分位数计算得到,因此区间端点通常是浮点数,Categories 中显示为 interval[float64, right];也就是说,同一份数据用 cut 切出的边界是等距的,用 qcut 切出的边界是等频的,箱子的宽度各不相同。
parameters 说明在这里同样简洁:q 既可以是整数(分成几等份),也可以给定分位点列表;labels 的用法与 cut 完全一致,用于把区间替换成易读的中文档位。
四、两种分箱方式怎么选
判断标准只有一个:你关心的是数值距离,还是样本数量。
业务规则明确、档位由数值区间定义(如 60 分及格、90 分优秀),用 pd.cut(),边界写死在 bins 里,可控性最好;
数据分布倾斜、需要保证每个箱子里样本量接近(建模时做特征分箱、分层抽样),用 pd.qcut(),避免出现空箱或极端不均衡的箱;
需要输出的结果带中文档位而非区间字符串时,两者都通过 labels 参数实现,输出的 category 可直接配合 value_counts() 做人数/占比统计。
五、边界、缺失值与结果类型
实际落地时最容易踩坑的是以下三点。
第一是区间开闭。pd.cut() 默认左开右闭,需要左闭右开时设置 right=False;分档边界与业务判定口径不一致时,务必先对齐这一点,否则恰好落在边界上的样本会被分错档。
- bins_2 = pd.cut(data, bins=bins, labels=labels, include_lowest=True, right=False) # 左闭右开
复制代码
第二是缺失值。如果原始列里含 NaN,分箱后这些位置仍然是 NaN,不会自动归入某个箱子。可以在分箱前用 fillna() 填充成指定值,也可以在统计阶段显式控制缺失值的处理:value_counts() 默认忽略 NaN,需要计入时设置 dropna=False。
第三是结果类型转换。分箱结果的 dtype 是 category,很多下游环节(导出 CSV、拼 SQL、写入模型特征)需要的是字符串或整数编码,可分别处理:
- str_bins = bins_2.astype(str) # category -> 字符串
- code_bins = bins_2.cat.codes # category -> 数值编码 0、1、2 ...
复制代码
cat.codes 按区间顺序给出 0 开始的整数编码,正好可以把有序分箱结果直接喂给模型;astype(str) 则保留标签文本,适合做展示和落库。
六、小结
pd.cut() 按数值宽度分箱,区间等距,适合分布均匀或业务规则明确的场景,配合 bins、labels、include_lowest、right 四个参数即可覆盖绝大多数分档需求;pd.qcut() 按样本数量分箱,区间等频,适合长尾分布数据,避免空箱。两者输出都是有序的 category,用 value_counts() 可以直接统计各箱样本量,再按需 astype(str) 或 cat.codes 转换类型,就能接上后续的报表或建模流程。 |