粗糙集属性约简:从决策表到最小属性集的可解释之路
简介面向数据挖掘学习者的粗糙集属性约简算法演示项目基于Python构建自带完整的图形可视化操作界面。程序通过main.py一键启动内置csv样例数据可直观展示决策表、属性约简过程与约简结果帮助理解粗糙集理论中的核属性、区分矩阵、约简求解等关键概念。资源共16个文件以py脚本为核心涵盖主程序、主页面控制、数据输入与统计模块另有pyc编译缓存、xml项目配置、txt使用说明及csv样例数据压缩包仅14KB轻量精简。项目采用工程化目录结构平台使用说明清楚标注运行注意事项——需在data_dimension_reduction目录下执行main.py且不可移动文件夹内文件避免路径错误。已有1216人学习/下载适合数据挖掘课程实验、算法对比研究或毕业设计参考可快速获得一套可运行的粗糙集可视化Demo。1. 粗糙集简约不是降维是「找最小必要属性集」从一张决策表说起如果你做过表格数据的分类建模大概率遇到过这种情况原始特征几十列真正对决策有贡献的却只有少数几个其余全是冗余或者高度相关。常规PCA / LDA这些方法处理的是数值分布压缩出来的是“线性组合后的潜变量”可解释性很差。而粗糙集属性约简也叫粗糙集简约完全换了一条路它不假设任何概率分布只用“对象在给定属性下是否可分辨”这个集合关系直接砍掉对决策没有贡献的条件属性。它面向的是决策表也就是“对象→属性→决策标签”这样的结构化数据。做完约简你得到的不是映射结果而是一个可以拿去写规则、解释给业务听的最小属性集。这也让可视化变得非常有价值等价类、正域、边界域、依赖度曲线都能直观看到算法在做什么。2. 粗糙集属性约简的核心逻辑正域、依赖度与约简的判定标准2.1 决策表、不可分辨关系与等价类粗糙集处理的对象是一张决策表通常写成四元组U 是对象集合AC∪D 是属性集合其中 C 是条件属性D 是决策属性V 是所有属性取值域f 是信息函数把每个对象映射到对应属性值。比如一张审批决策表U 是申请人C 是年龄、收入、信用D 是审批是否通过。在这里最核心的关系叫不可分辨关系。给定属性子集 B⊆C两个对象 x、y 关于 B 不可分辨指的是对 B 里的每个属性 a都有 f(x,a)f(y,a)。比如只看“年龄”和“信用”两个同为青年且信用良的人虽然在收入上不同但在 B 下是不可分辨的。这种不可分辨关系把 U 划分成若干等价类每个等价类里的对象在 B 上“长得一样”。等价类是后续所有计算的基石。正域、边界域、依赖度本质上都是在统计这些等价类里决策标签是否一致。如果某个等价类中所有对象的决策值完全一样这个类就属于正域如果一类里既有通过也有拒贷就落进边界域。粗糙集的“粗糙”不是指数据噪声大而是说在当前属性子集下存在无法被确定归类的边界对象。2.2 正域和依赖度怎么算一个7行表的算例直接看个例子。下面这张表共有 7 个对象条件属性是“年龄”“收入”“信用”都已经离散化为 0/1/2决策属性是“审批”1 表示通过0 表示拒贷。对象年龄收入信用审批u10001u20011u30111u41111u51120u62220u72200先看不带任何条件属性的情况整个 U 是一个等价类里面有两条决策1 和 0这个类属于边界域所以正域是空集依赖度是 0。带上全部条件属性 C{年龄,收入,信用} 后每个对象在三个属性上都不完全相同7 个对象各自成为一个等价类每个类里决策都唯一所以正域等于 U依赖度 γ(C,D)|POS_C(D)|/|U|7/71。现在尝试去掉某个属性。只看 B{年龄,收入}等价类划分是{u1,u2} 决策都是通过{u3} 通过{u4,u5} 一个通过一个拒贷属于边界域{u6,u7} 都是拒贷。正域对象是 u1、u2、u3、u6、u7共 5 个依赖度 5/7。所以 {年龄,收入} 达不到完全确定决策。再看 B{收入,信用}同样会得到边界对象依赖度小于 1。而 B{年龄,信用} 时等价类是 {u1}、{u2,u3}、{u4}、{u5}、{u6}、{u7}每个类里决策唯一依赖度正好等于 1。这说明在保持决策分辨能力不变的前提下属性“收入”是可以删掉的{年龄,信用} 就是一个约简结果。判断条件很清晰约简的属性子集 B 要满足 γ(B,D)γ(C,D)同时去掉 B 中任何一个属性依赖度都会下降。2.3 约简与核区分矩阵找必要属性约简这个概念和“看似重要但可替代”的冗余属性直接相关。如果 B 是一个约简说明 B 自身刚好够用但也可能不止一个约简。比如某些数据里 {年龄,收入} 和 {信用,收入} 都能达到同样依赖度且都不能再删那这张表就有两个约简。所有约简的交集叫核属性也就是必须保留、不可替代的属性。找核属性有一个很实用的工具叫区分矩阵。对任意两个对象 x 和 y如果它们的决策值不同就收集在条件属性上取值不同的那些属性放到矩阵位置 M(x,y) 里。如果某个位置只有一个属性 a那 a 就是核属性。因为要想区分这两个对象非 a 不可。整个约简问题可以理解成选一个最小的属性集合让它和每个 M(x,y) 都有交集。这个视角在属性不多时可以直接做集合覆盖穷举属性多的时候才需要启发式搜索。需要特别记住的是粗糙集约简并不保证你得到的是“预测准确率最高”的属性集而是保证“在训练数据上保持决策分辨能力不变的前提下属性最少”。这也是为什么后来会有基于邻域粗糙集、模糊粗糙集等变种用来兼容连续属性和噪声数据。3. 自己复现一个基于依赖度的启发式约简从决策表到最小属性集的 Python 代码3.1 连续属性离散化先决定“看到”哪些符号粗糙集天然处理符号型数据。连续数值必须先离散化再参与不可分辨关系计算。常见做法有三类等宽分箱、等频分箱、基于决策类别的有监督分箱。等宽分箱实现简单但遇到离群值会让很多对象挤进同一箱等频分箱每个箱的样本数接近相等更平衡有监督分箱则会参考决策标签尽量让每个箱里决策更纯。下面这个函数封装了前两种常见做法import pandas as pd def discretize_series(series, methodeq_freq, bins3): 将连续列离散化为整数分类码供粗糙集使用。 method: eq_freq等频分箱, eq_width等宽分箱 if method eq_freq: # qcut 按分位数切分每个箱样本量大致相等 return pd.qcut(series, qbins, labelsFalse, duplicatesdrop) elif method eq_width: # cut 按数值区间等宽切分 return pd.cut(series, binsbins, labelsFalse) raise ValueError(method 只支持 eq_freq / eq_width)这里 labelsFalse 表示直接返回整数编码因为粗糙集只需要比较取值相等还是不相等不需要保留区间含义。qcut 对存在大量重复值的列会报错duplicatesdrop 会自动减少分箱数。等宽分箱的 bins 可以传一个整数也可以传区间边界列表。一般来说bins 的取值对后续正域影响很大建议用交叉验证去做选择。拿到离散化结果后把它替换进原始 DataFrame 的对应列。注意决策列如果是连续值比如评分也要先离散化或按阈值切分否则正域计算会退化成“几乎每个对象一个决策值”依赖度虚高。3.2 计算正域与依赖度两个小而关键的函数要复现约简搜索最核心的物流就是正域计算。我用 pandas 的 groupby 按条件属性组合分组然后检查每组决策值是否唯一。为了避免索引错位我会保留原始 DataFrame 的行索引。def positive_region(df, cond_cols, dec_col): 返回正域对象索引集合。 条件属性相同的一组对象如果决策值唯一则都属于正域。 pos set() grouped df.groupby(cond_cols, sortFalse) for _, group in grouped: if group[dec_col].nunique() 1: pos.update(group.index.tolist()) return pos def dependence(df, cond_cols, dec_col): 依赖度 正域对象数 / 总对象数 return len(positive_region(df, cond_cols, dec_col)) / len(df)positive_region 里的 groupby(cond_cols) 会把条件属性取值完全相同的对象合并成一组sortFalse 保留原始出现顺序速度快一些。nunique() 统计决策列的唯一取值个数等于 1 说明这是一致等价类也就是正域的一部分。这里用 set 收集索引而不是行号防止重复对象出现时计数错误。依赖度是一种单调不减的指标你加入更多属性等价类只会变得更细正域不会缩小。所以依赖度适合做前向搜索的得分函数。但要注意如果数据本身有噪声依赖度永远到不了 1这时你要设定一个阈值比如 0.95而不是死等 1。3.3 向前贪心搜索找约简代码与参数说明有了依赖度函数就可以做最常用的顺序前向选择从空属性集开始每一步挑一个能让依赖度提升最多的属性加进来直到依赖度不再提升或者已经达到阈值。这个方案不保证全局最优但实现快适合高维数据。def greedy_reduct(df, cond_cols, dec_col, threshold1.0): 向前贪心属性约简。 threshold: 目标依赖度默认1.0数据有噪声时可降到0.95 selected [] remaining list(cond_cols) current_dep 0.0 while remaining: best_attr None best_dep current_dep for attr in remaining: dep dependence(df, selected [attr], dec_col) # 只接受带来严格提升的属性防止重复加冗余 if dep best_dep 1e-9: best_dep dep best_attr attr if best_attr is None: break selected.append(best_attr) remaining.remove(best_attr) current_dep best_dep print(加入属性, best_attr, 依赖度, round(current_dep, 4)) if current_dep threshold - 1e-9: break return selected, current_dep循环里计算依赖度的代价是 O(n·|selected|)整体复杂度约等于 O(|C|²·n)对于几千行、几十列的数据完全够用。参数 threshold 控制的是“我们认为达到充分分类的程度”默认 1.0 表示要求严格保持决策分辨能力实际做规则提取时阈值太高会导致约简保留过多属性太低又会损失信息。用第 2 章的 7 行决策表直接跑import pandas as pd df pd.DataFrame({ 年龄: [0, 0, 0, 1, 1, 2, 2], 收入: [0, 0, 1, 1, 1, 2, 2], 信用: [0, 1, 1, 1, 2, 2, 0], 审批: [1, 1, 1, 1, 0, 0, 0], }) conds [年龄, 收入, 信用] reduct, dep greedy_reduct(df, conds, 审批, threshold1.0) print(约简属性:, reduct, 依赖度:, dep)运行结果是先加“年龄”到 5/7再加“信用”到 1最后返回约简属性 [年龄, 信用]。这个结果和手算一致。需要说明的是pandas 的 groupby 默认会忽略 NaN 值所以如果条件列里有缺失值等价类划分会漏对象。下一章的避坑部分会专门讲这个。4. 图形可视化把等价类、依赖度和约简路径画出来4.1 决策表矩阵网格图一眼看出边界对象理论算完代码也跑通接下来就是让结果“看得见”。最直观的可视化叫作决策表网格图横轴放条件属性纵轴放对象单元格颜色深浅代表属性取值。然后我在网格上把不在正域里的对象加个红色边框边界情况一目了然。import matplotlib.pyplot as plt import numpy as np def plot_table_grid(df, cond_cols, dec_col): pos positive_region(df, cond_cols, dec_col) data df[cond_cols].astype(int).to_numpy() fig, ax plt.subplots(figsize(len(cond_cols) * 1.4, len(df) * 0.6)) im ax.imshow(data, cmapBlues, aspectauto) for r in range(data.shape[0]): for c in range(data.shape[1]): ax.text(c, r, data[r, c], hacenter, vacenter, fontsize10) # 给边界对象加红框 for row_idx, obj_idx in enumerate(df.index): if obj_idx not in pos: rect plt.Rectangle((-0.5, row_idx - 0.5), data.shape[1], 1, fillFalse, edgecolorred, lw2) ax.add_patch(rect) ax.set_xticks(range(len(cond_cols))) ax.set_xticklabels(cond_cols) ax.set_yticks(range(len(df))) ax.set_yticklabels(df.index) ax.set_title(决策表网格图红框对象属于边界域) return fig, ax这里的红色框对象就是当前属性子集下无法被确定分类的对象。约简的目标就是让红色框尽可能少。对比看用全部条件属性时红框为 0用 {年龄,收入} 时红框是 u4、u5换成 {年龄,信用} 后红框恢复为 0。可视化把“属性集有多大能力”变成了“图上有没有红框”用来向业务解释特别顺手。网格图最适合属性值少于 6 类的场景。如果离散化后有很多值颜色图会变得杂乱建议改成在格子内写数字、不使用连续 colormap。4.2 依赖度增长曲线看约简的边际收益做前向贪心搜索时我一般顺手记录每一步的依赖度变化画一条增长曲线。它能看到两个信息从无到有前几步依赖度提升有多猛最后几步还值不值得加属性。如果曲线已经平了再加属性就是过拟合。def dependency_trace(df, cond_cols, dec_col): selected [] remaining list(cond_cols) trace [0.0] while remaining: best_attr None best_dep trace[-1] for attr in remaining: dep dependence(df, selected [attr], dec_col) if dep best_dep 1e-9: best_dep dep best_attr attr if best_attr is None: break selected.append(best_attr) remaining.remove(best_attr) trace.append(best_dep) return selected, trace selected, trace dependency_trace(df, conds, 审批) plt.figure(figsize(6, 4)) plt.plot(range(len(trace)), trace, markero) plt.xticks(range(len(trace)), [空集] selected) plt.xlabel(已选属性) plt.ylabel(依赖度 γ) plt.title(前向贪心搜索过程中的依赖度变化) plt.grid(alpha0.3)这段曲线在只有一个约简的小数据上看起来就是 0→0.714→1.0一步一个台阶。真实数据上更常见的形状是前面陡峭后面变缓最后平顶。如果出现中间掉头下降就要检查代码依赖度理论上单调不减下降必然是因为离散化或者索引处理有 bug。横轴的标签是每一步被选中的属性名所以这条曲线也等于完整展示算法的决策路径。业务方看到曲线在前两个属性就到顶后面全平就会理解“为什么后面那些字段没必要建入模型”。4.3 约简前后规则数量对比条形图约简的直接收益是让后续规则表变小。一个等价类对应一条可能规则条件属性越多等价类切得越碎规则数越多。约简之后规则数通常会明显下降而且每条规则覆盖的对象数更多更稳定。这个对比用简单条形图就能表达。def rule_count(df, cond_cols): 统计条件组合的唯一数量 return df.groupby(cond_cols).ngroups original_count rule_count(df, conds) reduct_count rule_count(df, redTrue? )上面这个 redTrue 是故意写成占位符实际使用时直接传约简后的属性列表original_count rule_count(df, conds) reduct_count rule_count(df, reduct) plt.figure(figsize(6, 4)) plt.bar([原始属性集, 约简属性集], [original_count, reduct_count], color[#aaa, #5b9bd5]) plt.ylabel(唯一条件组合数) plt.title(约简前后等价类数量对比) for i, v in enumerate([original_count, reduct_count]): plt.text(i, v 0.1, str(v), hacenter) plt.show()注意“唯一条件组合数”并不等同于最终规则数因为同一个组合如果决策不唯一还会产生多条带置信度的规则。但作为对比它能直观说明约简如何减少建模复杂度和计算量。我在实际处理一张 50 个属性的表时约简后经常从几千个条件组合降到几十个这个条形图用来做报告摘要非常直接。5. 粗糙集约简落地中的5个常见坑与排查方法5.1 离散化分箱导致「假依赖」现象依赖度算出来是 1.0约简结果也很漂亮可是放到验证集上一看规则泛化能力很差。原因连续属性分箱数设得太多比如直接用 qcut 分成 10 箱很多对象被拆成单例等价类每个类里决策自然唯一正域虚高到接近全量。依赖度高不代表找出了真实规律只代表数据被切到足够碎。解决先用 3~5 箱做粗分观察依赖度变化再结合业务含义选边界。如果某个箱里的对象数过少说明分箱过细。更稳的方法是做有监督分箱让决策标签参与边界选择。切记依赖度要和规则泛化效果一起看不要单独看指标。5.2 缺失值被 groupby 悄悄忽略现象用 pandas 算正域时正域对象数比预期少依赖度偏低有的对象明明决策一致却没进入正域。原因groupby 在分组键含有 NaN 时会把缺失值对象单独放一组而且默认不显示。粗暴的结果是这些对象没有被并入正确的等价类甚至被丢掉。解决参与约简计算之前所有条件属性都做好缺失值处理。常见做法是用众数或均值补齐也可以显式将缺失值编码成独立符号比如 -1让缺失在等价类划分里被当作一种真实取值。决策列必须保证无缺失。检查依赖度异常偏低时先统计每列缺失值。5.3 贪心搜索停在局部最优不是最小约简现象向前贪心得到 4 个属性但用区分矩阵穷举后发现 3 个属性就能达到同样依赖度。原因依赖度函数是单调的但属性之间可能存在替代关系。贪心每一步都选当前收益最大的属性可能过早选入某个表现好的属性后面就再也发现不了能替代它的组合。解决属性不超过 20 个时优先用区分矩阵做集合覆盖穷举属性多时用贪心结果做初始化再做一个反向剪枝从已选属性里尝试逐个移除如果移除后依赖度不变就删掉。这样至少能去掉一部分冗余。还可以用多次随机起始顺序跑贪心取最短结果。5.4 索引错位让可视化对不上现象网格图第一行不是原本索引为 0 的对象或者规则提取出来的条件和表里对不上。原因DataFrame 在经过离散化、dropna、reset_index 后行位置变了但正域计算时用的是原始索引。如果后又用 range(len(df)) 去画图必然错位。解决从数据预处理开始就尽量保留原始索引。离散化函数里先 copy()不要原地覆盖如果必须 reset_index使用 reset_index(dropFalse) 把旧索引保留为单独列。正域函数中始终用 df.index 作为对象标识画图时用 enumerate(df.index) 得到每个格子的真实对象编号不要在函数里临时生成 0~n-1 的假索引。5.5 决策类别太多圈选边界域的图形难以阅读现象画网格图时红框都叠在一起颜色也分不清哪些对象属于哪一类整张图变成一片杂色。原因决策属性如果被切分了十几个类别matplotlib 的颜色表无法清楚区分边界对象一多红框叠加后盖住单元格可视化失去意义。解决先降低决策类别粒度比如把多分类映射成少数几个业务大类或只在图上显示一个特定的二元决策比如“是否通过”。红框可以改成在单元格左侧画竖线而不是围绕整行画方框。真正常见的做法是分图展示先画条件属性网格再用另一张散点图映射决策类别。6. 把约简结果变成可解释规则等价类转规则与置信度验证有了约简后的属性集下一步就是把它变成可以直接读的规则。我一般会把每个等价类转换成一条候选规则条件部分是约简属性取某个组合结论是组内多数决策置信度是多数决策占比。代码实现如下。def extract_rules(df, cond_cols, dec_col, min_conf0.8): rules [] for vals, group in df.groupby(cond_cols, sortFalse): counts group[dec_col].value_counts() total len(group) majority_cls counts.idxmax() conf counts[majority_cls] / total if conf min_conf: continue rules.append({ condition: dict(zip(cond_cols, vals)), decision: majority_cls, support: total, confidence: conf }) return rules rules extract_rules(df, reduct, 审批, min_conf0.8) for r in rules: print(r)这里 min_conf 是一个关键参数。设置太高会丢掉等价类中的低置信样本设置太低则会留下冲突明显的规则。常见做法是对原始条件属性集和约简属性集各提取一遍规则对比规则数和平均置信度约简后规则数应该更少平均置信度不应该明显下降。如果置信度掉太多说明约简把重要属性误删了需要回退到上一章介绍的区分矩阵穷举再验证。有了规则表你还能画一张规则决策图左侧是条件属性取值节点中间是规则节点右侧是决策类别节点。用 networkx 和 matplotlib 画有向图每种决策给一种颜色节点大小按 support 映射。这个图比表格更适合做交付展示也能快速发现哪些条件组合覆盖了大多数样本。最后回到一个基本原则约简只是数据压缩的预处理不是模型训练的终点。我最初做这个方向时也误把依赖度 1.0 当成“完美表格”后来才发现那是离散化过细造成的幻觉。真正靠谱的习惯是对照原始依赖度、记录每种分箱参数下的约简结果和验证集上的规则置信度再一起决定要不要采用这组属性。这三张图——网格图、依赖度曲线、规则对比图就是我判断约简质量的常规三件套。希望帮到你。本文还有配套的精品资源点击获取