资讯动态

匿名表格数据建模:单调性检查与秩变换提升XGBoost AUC

发布时间:2026/10/17 19:48:53 来源:尧图企业网站定制
简介这是一套面向数据分析和机器学习学习者的桑坦德客户满意度竞赛项目围绕Kaggle经典赛题展开使用Jupyter Notebook完成从数据探索到模型构建的完整流程。压缩包共19个文件约21.23MB包含7个CSV格式的训练/测试数据与预测结果、6个已保存的机器学习模型AdaBoost、梯度提升、决策树、随机森林、Logit和SVM、2个Python脚本及对应pyc缓存以及1个主分析Notebook和配套README同时还提供网格搜索结果记录便于对比不同模型的效果。项目通过加载预览、清洗缺失值、特征工程、EDA可视化和多模型训练评估等环节系统展示了处理二分类满意度问题的标准思路。目前已有223人学习下载适合希望掌握Pandas数据处理、Scikit-learn建模与调参技巧的初学者或竞赛入门者。读者可从中获得完整可复现的代码方案、模型对比结果与实际调参经验按Notebook逐步运行即可理解各步骤的设计逻辑。1. 这个匿名数据项目到底在预测什么371 列特征背后的客户满意度二分类Project---Santander-Customer-Satisfaction 这份数据表面上是某银行客户满意度调研产出的三百多列匿名数值实质是一道标准二分类题用 371 列含 ID 和 TARGET预测一个客户会不会给差评。训练集 76020 行TARGET 里约 96% 是满意客户0不满意的只有约 3.6%线上评估只看 AUC。我第一次拿到时也对着黑匣子发过呆——特征全部脱敏、没有任何业务字段说明、连缺失值都几乎没有仿佛只能靠玄学调参。做完一轮才发现这类全匿名表格数据的套路恰恰是最固定的先确认目标分布和特征与目标之间的单调关系再做保守清洗与秩变换最后用梯度提升树把 AUC 推到 0.82 以上。这篇就把这套流程完整拆开适合正在做表格分类、想搞清匿名特征该怎么下手的工程师也适合对照自己的项目踩坑。2. 先看数据再动手目标分布、特征单调性与基线 AUC2.1 读入数据与目标分布为什么说这是最难做的简单分类拿到数据第一步永远不是建模而是确认三件事形状、缺失、目标比例。这份数据的标准形态是训练集 76020 行、371 列其中 ID 是行号、TARGET 是标签剩下 369 列几乎全是 float没有 categorical 列。这意味着 pandas 的 describe 和 value_counts 基本就能把数据摸干净不需要复杂的数据字典。真正要留神的是正例只有 2700 多个任何按标签分组的统计量都会因为样本太少而剧烈抖动后面所有分箱、编码操作都得把这个分母记在脑子里。import pandas as pd import numpy as np df pd.read_csv(train.csv) print(df.shape) # (76020, 371) print(df.dtypes.value_counts()) y df[TARGET] print(y.value_counts(normalizeTrue)) # 0 ~ 0.9636 # 1 ~ 0.0364 print(缺失值总数:, df.isna().sum().sum())这段代码做三件事。shape 先确认列数防止读文件时多带索引列dtypes 的 value_counts 可以看到绝大多数列是 float说明数据做过数值化脱敏value_counts(normalizeTrue) 直接给出正负样本比例1 的比例约 3.6%这个数字是后面所有决策的分母。缺失值总数打印出来是 0但不要高兴太早——没有 NaN 不代表没有脏值我习惯紧接着看每列的取值分布尤其是那种某个值占了绝大多数的列。print(df[var3].value_counts().head(5))var3 是这份数据里最经典的异常列值 2 占了七万多行-999999 只占一小撮。这种单值绝对主导 少量人造极端值的形态在匿名数据里基本是缺失标记或者业务上的特殊状态。它不叫缺失但比缺失更坑因为树模型会认真拿它做分裂条件白白浪费深度。这个点第 3 章专门处理现在先记下来。这个数据还有一个容易误导新手的属性评估指标是 AUC不是 accuracy。96% 的负样本意味着你全预测 0 也能拿到 96% 准确率但这在 AUC 面前毫无意义。AUC 只看正样本的预测分数是否排在负样本前面所以它天然免疫类别不平衡也意味着我们不需要上来就做采样、调阈值。这点想通之后整个建模目标就变得很纯粹把正样本的概率排名往前提。2.2 特征与目标的单调关系这份数据最有价值的先验信息这个项目最出名也最容易被忽略的先验是几乎所有特征和目标之间都存在单调关系。把任意一个特征按分位数切成 20 箱算每个箱里 TARGET 的均值你会发现箱号越大 TARGET 均值越高或越低中间既没有倒 U也没有反复震荡。这说明这些特征不是原始业务字段而是经过某种统计变换生成的量信息已经按目标排好序了。这个先验直接决定预处理策略分箱、秩变换、特征选择全部以单调为前提而不是以传统分布形态为前提。def bucket_target_mean(series, target, bins20): cut pd.qcut(series.rank(methodfirst), bins, labelsFalse) tmp pd.DataFrame({b: cut, y: target}) return tmp.groupby(b)[y].mean() means bucket_target_mean(df[var15], y) print(means.round(4)) trend np.corrcoef(np.arange(len(means)), means)[0, 1] print(box index vs target mean:, round(trend, 3))这里用 rank 而不是原始值做 qcut是因为特征里有极端值会带偏分位点groupby mean 得到 20 个箱的目标均值序列最后算箱序号和箱均值的相关系数接近 ±1 说明单调性强。把 369 列全跑一遍你会发现绝大多数特征的相关系数绝对值超过 0.9。注意这个相关性是箱均值和目标均值的不是特征值和目标值的直接相关所以它不代表强预测力只代表结构稳定。bins20 是经验值。76020 行切 20 箱每箱约 3800 行目标均值足够稳如果把 bins 放到 50尾部箱只剩几百行均值开始乱跳单调趋势被噪声打断。后面所有分箱都用 20除非某个特征取值种类极少那种列可以直接看原始 value_counts不需要分箱。这个先验还有一个推论哪些特征真正有用看它的箱均值曲线跨度就知道。跨度大的特征正样本概率从最低箱到最高箱可能差 10 倍跨度小的基本是噪声列。我一般会把所有特征的跨度排序尾部那些曲线几乎水平的列在后续建模里基本不贡献可以留着不处理也可以删除对结果影响很小。2.3 用逻辑回归建立基线先有一个能跑的分数任何预处理和调参都要有对照系。对这个项目我习惯先用逻辑回归在原始特征上跑一个 OOF AUC不做特征选择、不做清洗。原因很简单LR 对尺度敏感、对噪声敏感如果 LR 在某个特征集上分数明显提升说明改进来自特征本身反过来如果 LR 没变化、XGB 却涨了那多半是树结构在起作用。两个模型互为参照比单看一个模型更容易判断每一步操作的真实收益也避免你把树的拟合能力误当成特征工程的功劳。from sklearn.model_selection import StratifiedKFold from sklearn.linear_model import LogisticRegression from sklearn.preprocessing import StandardScaler from sklearn.metrics import roc_auc_score X df.drop(columns[ID, TARGET]) y df[TARGET] folds StratifiedKFold(n_splits5, shuffleTrue, random_state42) oof_lr np.zeros(len(df)) for tr, va in folds.split(X, y): scaler StandardScaler().fit(X.iloc[tr]) X_tr scaler.transform(X.iloc[tr]) X_va scaler.transform(X.iloc[va]) lr LogisticRegression(C0.1, max_iter1000) lr.fit(X_tr, y.iloc[tr]) oof_lr[va] lr.predict_proba(X_va)[:, 1] print(LR OOF AUC:, round(roc_auc_score(y, oof_lr), 4))这里有三个细节必须强调。第一StratifiedKFold 保证每折正例比例一致对 3.6% 正例的数据这是底线普通 KFold 可能把某一折的正例分到只剩 300 个。第二StandardScaler 必须在训练折上 fit再 transform 验证折整份数据统一 fit 会引入验证信息属于最常见的隐性泄漏。第三C0.1 是正则偏强的起点369 列匿名特征之间存在大量共线默认 C1 容易让 LR 去拟合特征间的噪声协方差。裸 LR 在这个数据上 OOF AUC 大约在 0.77~0.79 之间这就是原始特征下线性模型的真实水平。这个分数本身已经说明一件事特征里包含的信息量是足够的纯线性模型不加任何交互就能到 0.78。问题只出在噪声和尺度上这正是第 3 章要解决的。如果 LR 基线连 0.7 都不到我会先怀疑数据读错了或者标签方向反了而不是继续往下调参。3. 特征清洗与离散化把单调特征变成模型能吃透的形态3.1 为什么对匿名特征做分箱要基于目标均值一般特征分箱等宽、等频的目的只是把连续值粗化减少过拟合但在这份数据上分箱的真正目的是隔离噪声。因为特征与目标单调每个箱的 TARGET 均值本应平滑递增一旦某个箱的均值突然掉头基本就是极端值或者空值标记在作怪。所以这里的分箱不是造特征而是质检用箱均值曲线反推特征质量。做法是当前折上算箱均值、验证折上应用而不是全量计算否则就变成了泄漏。另一个必须处理的是 var3 里的 -999999。这种人造极端值在匿名数据里几乎总是脏标记可能是缺失值编码也可能是某种特殊账户状态。把它留在列里LR 的系数会被这一个值扯动树模型则会专门为它切一个分支而这个分支在测试集上对应的样本语义未必相同。常见做法是替换成该列众数。替换之后 var3 基本退化成常数列对模型不贡献信息但至少不再制造假分裂。3.2 处理 -999999 与秩变换两个可复制的操作先解决 var3 的极端值这一步逻辑简单但顺序有讲究# 1) 把 var3 的 -999999 视为缺失 fill_val df[var3].mode()[0] # 众数通常是 2 df[var3] df[var3].replace(-999999, np.nan) df[var3] df[var3].fillna(fill_val) print(df[var3].value_counts().head(3))fill_val 必须在训练集上先算好测试集直接用同一个值填绝不可以在测试集上重新算众数。原因很直白测试集是一个没有标签的样本任何从测试集统计出来的值都会让你的预处理管线在线上部署时无法复现。替换完打印 value_counts 是为了确认列已经退化为近常数后面建模时这列基本不会被选中分裂。第二步是秩变换这是这份数据上我几乎必做的一步from scipy.stats import rankdata def rank_transform(X_df): arr X_df.to_numpy(dtypefloat) for j in range(arr.shape[1]): arr[:, j] rankdata(arr[:, j]) / arr.shape[0] return arr对每一列做百分位秩归一化输出均匀分布在 0 到 1 之间。这和前文说的单调结构天然匹配特征值的高低顺序就是目标概率的高低顺序秩变换保住顺序、丢掉原始间距而原始间距里恰恰藏着极端值噪声。对树模型来说分裂只依赖排序所以秩变换后 XGB 的分数变化不大对 LR 和神经网络这个变换通常能带来 0.005~0.01 的 AUC 提升因为它同时解决了尺度问题和长尾问题。rankdata 的 methodaverage 处理并列值等价的值取平均排名不会人为造出虚假顺序。这一步在训练集和测试集上要独立做还是那句话各自用自己的分布算排名不要拿训练集的排名参数去映射测试集因为测试集的行数不同、分布也可能有偏移。如果你用的是 sklearn 的 RankGauss 或者自定义函数注意它 fit 之后 transform 的行为本质上和这里一样。做完这两步我通常还会顺手删掉方差接近 0 的列。秩变换之后常数列会变成均匀分布的伪随机数——注意这不是信息这是噪声。所以删除要在变换之前做判断依据是变换前的原始取值种类数比如 var3 替换后基本只有一两个不同值直接 drop别让它进模型。3.3 清洗前后的 AUC 对比什么时候该停手清洗有没有效果不能靠感觉要在同一套折上对比。下面这段代码直接复用 2.3 的 folds 对象把秩变换后的数组喂给同一个 LR 配置X_clean rank_transform(df.drop(columns[ID, TARGET])) oof_lr_clean np.zeros(len(df)) for tr, va in folds.split(X_clean, y): scaler StandardScaler().fit(X_clean[tr]) lr LogisticRegression(C0.1, max_iter1000) lr.fit(scaler.transform(X_clean[tr]), y.iloc[tr]) oof_lr_clean[va] lr.predict_proba(scaler.transform(X_clean[va]))[:, 1] print(LR clean OOF AUC:, round(roc_auc_score(y, oof_lr_clean), 4))把结果放到一起看节奏非常清楚方案LR OOF AUC说明原始特征约 0.78含极端值噪声尺度不统一替换 var3 后约 0.79去掉人造极端值系数更稳再秩变换约 0.80尺度统一长尾收敛线性可分性提升每一步提升大概在 0.005~0.01这是正常节奏。真正难的是知道什么时候该停手。我的判断标准就一条当一步操作带来的提升小于 0.002并且代码开始为了这 0.002 增加复杂度时立刻停。这个量级的提升大概率是噪声拟合换个 CV 种子就消失。所有对比都必须在同一套折、同一随机种子下进行这比具体的预处理技巧更重要。还有一类操作要特别警惕对这个数据做 PCA 或者聚类特征。特征本身是单调统计量PCA 会把这种结构打散聚类则因为样本量 7.6 万、正例太少而极不稳定基本只能做出运气成分。我见过有人在这里花了大量时间最后 AUC 变化在正负 0.001 之间晃属于典型的白忙活。清洗做到秩变换这一步已经足够剩余的空间留给第 4 章的树模型。4. 用 XGBoost 把分数推上去参数与调参顺序4.1 为什么梯度提升树是这类表格数据的主模型LR 到 0.80 之后基本到顶瓶颈在于特征间存在大量非线性交互而匿名特征又没有业务语义让你手工构造交互项。树模型的自动分裂正好接手这件事它会在训练中自动找到这个特征大于某个分位且另一个特征小于某个分位这样的组合条件。XGBoost、LightGBM、CatBoost 三选一我选 XGB 的原因很实际这个数据量76020 行、369 列用 hist 算法跑得足够快调参资料最全而且它对缺省参数更宽容不容易因为一两个参数没设对就在种子间剧烈波动。LightGBM 在同样数据上通常更快但 leaf-wise 生长策略在这个强单调结构的数据上容易过拟合需要更紧的正则CatBoost 的有序提升在这个纯数值、无类别特征的项目上没有发挥空间。所以主模型选 XGBLightGBM 留到最后一章做融合的多样性来源。4.2 一组能直接跑的核心参数与早停配置下面这组参数是我在这个项目上反复用、每次都能跑到 0.82~0.83 的起点配置。直接看代码from xgboost import XGBClassifier oof_xgb np.zeros(len(df)) for tr, va in folds.split(X_clean, y): model XGBClassifier( n_estimators3000, learning_rate0.02, max_depth6, subsample0.85, colsample_bytree0.8, min_child_weight5, reg_lambda1.0, eval_metricauc, tree_methodhist, n_jobs-1, random_state2024, ) model.fit( X_clean[tr], y.iloc[tr], eval_set[(X_clean[va], y.iloc[va])], early_stopping_rounds100, verboseFalse, ) oof_xgb[va] model.predict_proba(X_clean[va])[:, 1] print(XGB OOF AUC:, round(roc_auc_score(y, oof_xgb), 4))X_clean 是第 3 章秩变换后的 numpy 数组。早停用验证折决定最佳迭代数100 轮 patience 在 lr0.02 下相当于允许模型在验证集上连续 100 轮不进步才停这个窗口足够宽不会因为单轮抖动就提前截断。用验证折做早停会让 OOF AUC 有一点点乐观通常偏差小于 0.002可接受但要知道它存在第 5 章的陷阱四会专门讲这个落差的处理。参数的选择思路比参数本身更重要。先看这张表参数取值作用learning_rate0.02小学习率让每个树只做微调配合早停控制迭代数max_depth6控制单棵树复杂度5~8 都是平坦区subsample0.85行采样降低每棵树的方差colsample_bytree0.8列采样369 列下增加树间多样性min_child_weight5拦住收益过小的分裂是对匿名噪声最有效的正则reg_lambda1.0L2 正则和 min_child_weight 配合tree_methodhist近似分箱直方图7.6 万行下速度提升明显最重要的两个是 max_depth 和 min_child_weight。max_depth 等于 6 在这份数据上是明显的平坦区小于 5 欠拟合大于 8 开始把单调结构里的细碎噪声也学进去OOF 反而下降。min_child_weight5 是关键中的关键匿名数据里大量分裂的收益极小这个参数要求每个叶子节点至少积累 5 的样本权重和把这些噪声分裂直接挡在门外。调参顺序我一般是先定 lr 和早停然后只动 max_depth 和 min_child_weight确认这两个的平坦区后再试 subsample 和 colsample_bytree最后才碰 reg_lambda。不要一上来就 grid search 全排列参数之间的交互在这个样本量下很容易把你带进局部最优。这里还有一个容易被忽略的点我没有在参数里写 scale_pos_weight。原因前面说过AUC 是排序指标类别权重只会改变模型输出概率的尺度不会改变排序质量。如果你后面要接业务阈值可以单独试 scale_pos_weight3 这类温和取值但判断依据永远是 OOF AUC 而不是召回率。4.3 阈值的选择在 96% 负样本下怎么定 cutoffAUC 只回答排序质量不回答概率超过多少算不满意。真实业务需要一个二分类标签时才会涉及阈值。先把结论说清楚在这个 96% 负样本的数据上模型输出的概率整体偏低如果你把阈值定在 0.5召回率会非常难看因为正样本的预测分数大多在 0.1 到 0.4 之间。常见做法是根本不设阈值直接按概率排序取前 5% 或者按客服工位预算取前 N 个。如果一定要一个硬阈值就在 OOF 概率上画曲线别去测试集上调。from sklearn.metrics import precision_score, recall_score for cut in [0.05, 0.10, 0.20, 0.30, 0.50]: pred (oof_xgb cut).astype(int) p precision_score(y, pred, zero_division0) r recall_score(y, pred, zero_division0) print(fcut{cut:.2f} precision{p:.3f} recall{r:.3f})这段代码的输出会呈现一个典型的规律cut 越高精确率越高、召回率越低cut 越低召回率上升但精确率崩得很快。具体选哪个点取决于业务上漏掉一个不满意客户和错打一个满意客户电话哪个代价更大。满意度场景通常更怕漏掉不满客户所以阈值会往 0.1 甚至更低压代价是精确率下降、客服工作量上升。zero_division0 是防止某个 cutoff 下完全没有正预测时 precision 报错。阈值选定后还有一个稳定性校验把 5 折各自的阈值-精确率曲线叠在一起画选 5 折走势都一致的区间不要选某单折最优的那个点。单折最优几乎总是运气5 折一致才是数据本身的规律。这个校验做完阈值这件事才算真正落地。5. 避坑指南这个项目上最容易翻车的 5 个细节前面流程可以稳定跑到 0.82~0.83但我在这个项目上见过不少翻车案例问题高度集中在 5 个点上。每一条都是血泪经验按现象 → 原因 → 解决拆开讲。5.1 陷阱一把 ID 列当特征训练分数虚高现象有人图省事直接拿全表 fit训练集 AUC 能到 0.99OOF 却只有 0.83线上更是一塌糊涂。还有人模型的 feature_importance 里 ID 排第一怎么正则都压不下去。原因ID 是行号每行一个唯一值树模型可以在分裂时把 ID 当成记忆索引直接查表把训练样本的标签背下来。早停根本拦不住这种捷径因为它在验证集上同样有效——验证集的 ID 也是唯一的。解决建模前务必 drop(ID)测试集同样要丢否则线上特征数量对不上直接报错。这里没有例外哪怕你觉得排第一的特征被我删了我吃亏也删。ID 在这份数据里除了当行号没有任何语义留着只有坏处。5.2 陷阱二用全量 target encoding 把单调特征变成标签复印件现象有人把全部特征做目标均值编码后线下 AUC 冲到 0.85比 XGB 还高。一上测试集崩回 0.82然后开始怀疑是模型不够复杂越调越崩。原因全量训练集上算目标均值等于把标签直接抄进特征。这份数据的特征和目标强单调泄漏会被放大好几倍所以线下分数才虚高得离谱。测试集没有标签可抄分数自然打回原形。解决这类全数值单调特征根本不需要目标编码第 3 章的秩变换已经吃透了单调结构。如果非要编码只能在每折训练集上计算、验证折上映射并且只对离散化后的高基数特征做不做全集。做任何基于标签的编码之前先问自己一句这个操作在测试集上能复现吗不复现的就是泄漏。5.3 陷阱三交叉验证种子不固定调参调了个寂寞现象同一组参数random_state 从 42 换成 2024OOF AUC 从 0.833 变成 0.831。你以为是参数波动实际是种子波动一来一回白调半天。原因正例只有 2700 多个5 折里每一折正例的构成差异都会带来 0.002~0.005 的波动这个幅度和参数调整带来的变化是同一量级的。不固定种子你根本分不清分数的变化来自参数还是来自运气。解决全局固定一个 CV 骨架——同一个 folds 对象从头用到尾所有对比都在同一划分上做。最后一轮评估时再用 5 个不同种子各跑一遍报告均值加标准差而不是单次最好值。如果标准差超过 0.003说明你的模型本身不稳定先加正则别急着细调参数。5.4 陷阱四全量重训后线上分数比 CV 低怀疑自己过拟合现象OOF AUC 0.833全量重训后提交线上 0.831。有人立刻开始删特征、加正则结果越改越低最后连 0.83 都回不去。原因OOF 因为用验证折做早停本来就带一点乐观偏差全量重训只是把这个偏差抹掉0.002 的落差是正常量级不是过拟合的信号。把重训结果和 OOF 严格对齐反而说明你的验证流程是诚实的。解决把全量重训当成交付动作而不是验证动作。验证只看 OOF重训时用 CV 平均最佳迭代数再乘 1.1固定种子跑完直接出概率文件。重训之后不再做任何选型判断不回头对比、不反复试参数否则你一定会掉进用测试集调参的坑。这个习惯能避免大量自我怀疑。5.5 陷阱五正例太少就上 SMOTE结果 AUC 原地踏步现象看到 3.6% 的正例比例第一反应是数据不平衡要过采样。SMOTE 之后召回率从 0.5 涨到 0.7提交线上分数却一动不动甚至下降。原因评估指标是 AUC它只关心排序不关心召回率。SMOTE 生成的是插值样本会改变正常样本在特征空间里的相对位置关系排序质量不升反降。召回率涨是因为合成样本把决策边界推宽了而 AUC 衡量的正是排序所以它最诚实。解决3.6% 正例在 76020 行里对应 2700 多个样本这个数量足够训练不需要过采样。如果实在担心类别信息丢失用 scale_pos_weight3 这种温和权重试一次看 OOF AUC 变化涨就留不涨就删。任何改变样本分布的操作最后都要回到 OOF AUC 上接受审判。6. 最后一步融合与全量重训把模型落成可交付的预测器6.1 五折预测拼接线下验证与线上交付的一致性OOF 拼接是整个流程最关键的产物。5 折模型分别预测验证折拼成一条和训练集等长的概率向量这条向量既用来算最终 AUC也用来定阈值、定融合权重。交付时用的模型是全量重训模型但重训模型的一切选择依据都来自 OOF 这条向量。融合层面常见做法是 XGB 和 LightGBM 各跑 3 个种子在 OOF 上搜索最优加权权重权重大概在 XGB 0.7、LGBM 0.3 附近然后把权重固定死不再回头调。融合提升通常有 0.002~0.005但前提是两模型的预测相关性够低。6.2 一份可交付的推理代码骨架def load_and_predict(test_csv, output_csv): test pd.read_csv(test_csv) ids test[ID] X_test test.drop(columns[ID]) if TARGET in X_test.columns: X_test X_test.drop(columns[TARGET]) model XGBClassifier() model.load_model(final_model.ubj) prob model.predict_proba(X_test)[:, 1] pd.DataFrame({ID: ids, TARGET: prob}).to_csv(output_csv, indexFalse) print(saved:, output_csv)推理时不设阈值直接输出概率因为评估是 AUC排序即答案。两个容易漏的细节一是训练时对 var3 做的替换值、秩变换的列顺序要存成配置文件或者 pickle测试集必须走完全相同的变换二是保存 XGB 模型用 save_model 的原生格式 .ubj不要用 pickle 存 XGB 模型跨版本反序列化经常崩。load_model 直接读原生格式就稳。6.3 交付前的自检清单检查项做法通过标准特征一致性对比训练集和测试集 drop 后的列名集合完全一致预处理可复现var3 填充值和秩变换脚本固化为一个函数跑两遍结果一致多种子稳定性5 个种子各跑一遍 OOFAUC 标准差小于 0.002预期差控制全量重训模型在小验证集上的 AUC 与 CV 均值对比差值小于 0.005这套自检做完模型才算真正交付。我现在的习惯是拿到任何匿名表格数据第一件事就是跑单调性检查和 LR 基线再决定要不要上树。这个习惯就是从这个项目上养成的它帮我过滤掉了一大半无效的预处理操作。希望这篇能帮你在同样的数据上少走几步冤枉路直接做到能交付的水平。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑