很多同学做机器学习的时候,很多同学都遇到这样的困惑,这次用随机森林、GBDT,还是直接上XGBoost?
但实际项目里,真正容易出问题的,往往不是模型选得不够高级,而是前面的数据处理顺序没搞对。
最后AUC、Accuracy看着很好,结果却未必可信。
所以我一直比较建议大家:先把数据处理流程理清楚,再去纠结模型。
最近刚好有同学问到这样的问题,咱们一起来看下,也欢迎大家加入我们的学习圈子~
个问题很典型,下面就借着这段对话,把这几个容易踩坑的地方聊清楚~
同学提问
原文:https://www.coszhuang.cn/questions/871
主题:机器学习方法的选择、以及数据异常值的处理
我一共有1700个样本,分类问题进行机器学习,GBDT、XGBoost或其他方法哪个更适合呢? 收集完数据之后,需要进行异常值和缺失值的处理,这两部都需要在划分训练集和测试集之前进行吗?请问划分训练集和测试集之前需要做什么呢?
大壮回复
事实上很多同学,尤其是刚开始做机器学习项目的时候,很容易把注意力全部放在:我到底用 GBDT,还是 XGBoost
但实际做项目以后你会发现,1700条数据用什么模型,可能只是第二重要的问题;第一重要的是数据怎么切、预处理怎么做,以及有没有发生数据泄漏。
尤其是你第二个问题,处理顺序如果错了,模型最后的测试指标可能会“虚高”。
咱们分开聊,你参考看看~
第1,1700个样本,分类问题到底选什么模型?
先说一个结论:
单纯知道“有1700个样本”,还不能决定一定用 GBDT 或 XGBoost。
模型选择还得看几个东西:
-
特征有多少个; -
数值特征多还是类别特征多; -
类别是否平衡; -
特征和标签之间是不是存在明显非线性关系; -
你更看重准确率,还是模型解释性。
但1700个样本确实不算特别多,所以我一般不太建议一上来就考虑深度学习。
这种规模的数据,传统机器学习反而很合适。
我自己通常会先跑这么几类:Logistic Regression → Random Forest → GBDT → XGBoost / CatBoost
为什么连逻辑回归都要跑?
因为它是一个非常好的Baseline。
比如你最后发现:
Logistic Regression:AUC 0.82
Random Forest: AUC 0.84
XGBoost: AUC 0.85
这时候就得想想:
为了0.01~0.03的提升,有没有必要上更复杂的模型?
而不是看到XGBoost名字高级,就直接用XGBoost。
那GBDT和XGBoost怎么选?
如果你的数据主要是结构化表格数据,1700条样本,我建议优先试,XGBoost、CatBoost、Random Forest。
其中:
如果主要是数值特征,而且特征之间有明显非线性关系,XGBoost通常值得重点尝试。
如果类别特征特别多,比如:
性别
地区
职业
设备类型
疾病类型
产品类型
这种情况下可以重点试试 CatBoost。
如果想先快速做一个稳定的Baseline,Random Forest非常适合,调参也相对简单。
GBDT当然也能用,只不过现在实际项目里,如果让我二选一,我通常会更倾向于XGBoost。
不过有一点特别重要:
1700条数据并不多,XGBoost参数不要调得太激进。
比如树特别深:
max_depth=10
再加上:
n_estimators=1000
很容易把训练集学得非常漂亮,然后测试集一塌糊涂。
1700条数据更应该关注的是:
交叉验证结果稳不稳定。
比如不要只看一次训练测试划分:
这次AUC = 0.86
就下结论。
更建议看5折分层交叉验证:
Fold1:0.83
Fold2:0.85
Fold3:0.79
Fold4:0.84
Fold5:0.82
这时候平均AUC大约0.83,而且波动也能看出来。
如果另一个模型:
0.84
0.84
0.83
0.85
0.84
虽然最高分没那么高,但我反而可能更喜欢第二个。
因为它更稳定~
第2,缺失值和异常值,要不要在划分训练集之前处理?
这里是整个问题里最应该注意的地方。
我的建议是,只要某个数据处理步骤需要“从数据中学习参数”,原则上都应该先划分训练集和测试集,再只用训练集学习。
这句话一定要记住。
比如你现在1700条数据。
先:
X_train, X_test, y_train, y_test = train_test_split(...)
然后再处理:
缺失值
异常值
标准化
归一化
特征选择
PCA
SMOTE
而不是反过来。
为什么?
举个特别简单的例子。
假设某个特征有缺失值,你准备使用中位数填补。
如果你先拿1700条完整数据计算:
中位数 = 28.6
然后再划分训练集和测试集。
实际上这个28.6里面已经包含了测试集的信息。
虽然只泄漏了一点点,但它本质上就是:
训练阶段偷看测试集。
正确方法应该是:
只拿训练集计算:
训练集的中位数 = 28.2
然后:
训练集缺失值 → 28.2
测试集缺失值 → 同样使用28.2
测试集只能被“处理”,不能参与规则的制定。
第三,异常值也是一个道理
很多同学特别容易在这里犯错。
比如拿全部数据计算:
Q1
Q3
IQR
均值
标准差
然后把异常值删掉,再切训练集测试集。
严格来说,这也存在数据泄漏。
如果你准备使用IQR:
然后规定:
这两个阈值最好也是:
在训练集上计算。
再使用相同阈值处理测试集。
不过这里还有一个实际项目中特别重要的问题:
不要看到异常值就删
异常值不一定是错误数据。
比如:
用户月消费:50000元
是不是异常?
统计上可能是。
但如果这是一个真实的高净值客户,那它反而可能是最重要的数据。
再比如设备故障预测:
正常温度:40℃
某条数据:95℃
如果你的目标本来就是预测故障,你把95℃删掉了,那模型还学什么?
所以实际项目里我一般会区分两类。
一种是明显错误数据:
年龄 = -20
身高 = 950cm
日期 = 2099-99-99
传感器数值超出物理范围
这种属于业务规则错误,可以直接清理。
另一种是:
统计意义上的极端值
这种不要着急删。
特别是Random Forest、GBDT、XGBoost这种树模型,本身对于极端值就比线性回归、KNN这一类模型更不敏感。
很多时候采用截尾处理,比直接删除样本更稳。
第4,那划分训练集之前,到底能做什么?
我一般会把数据处理分成两个阶段。
划分之前,可以做这些
主要是一些不依赖数据分布的确定性清洗。
比如:
删除完全重复的数据
删除明显错误记录
统一字段格式
统一单位
修正字段类型
检查标签错误
检查ID泄漏
检查重复患者/重复用户
比如:
身高:
1.75米
175厘米
这种单位统一,可以先处理。
或者:
男
male
M
男性
统一成同一个类别,也没有问题。
因为这些规则并不是通过计算整个数据集的统计特征得到的。
划分之后再做这些
只要涉及“学习数据分布”,最好都在训练集里完成:
均值填补
中位数填补
众数填补
IQR异常值处理
Z-score异常值处理
标准化
MinMax归一化
PCA
特征选择
SMOTE
降维
可以把整个流程理解成:
原始数据
↓
基础清洗
↓
训练集 / 测试集划分
↓
只使用训练集学习预处理规则
↓
训练集进行处理
↓
使用同一规则处理测试集
↓
训练模型
↓
最终测试
这就是为什么现在实际项目里,我比较推荐大家使用 Pipeline。
它可以很大程度避免不小心发生数据泄漏。
第5, 1700条数据,我会怎么做?
如果是我拿到这个项目,我大概会这样设计:
1700条样本
↓
基础数据检查
↓
分层划分训练集和测试集
80% / 20%
↓
训练集约1360条
测试集约340条
↓
训练集内部做5折StratifiedKFold
↓
缺失值 / 异常值 / 编码等全部放入Pipeline
↓
比较模型
然后至少比较:
Logistic Regression
Random Forest
XGBoost
如果类别变量比较多,再增加:
CatBoost
最终模型不是看谁某一次跑得最高,而是看:
平均指标
交叉验证标准差
训练集和验证集差距
最终测试集表现
如果分类特别不平衡,例如:
正常:1600
异常:100
这时候就不要只看Accuracy了。
因为模型全部预测正常:
Accuracy = 94.1%
看起来非常高,其实一点用没有。
这类数据应该重点看:
Recall
Precision
F1
ROC-AUC
PR-AUC
尤其正样本很少的时候,我会比较关注 PR-AUC和Recall。
第6,用自己的一个例子你可以感受下
我按照从你这里获取的信息,尽可能的贴合,把最重要的几件事串起来:
-
先划分训练集测试集; -
缺失值只通过训练集学习; -
异常值阈值只通过训练集学习; -
使用分层交叉验证; -
比较多个模型; -
画出模型交叉验证结果和最终测试结果。
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split, StratifiedKFold, cross_val_score
from sklearn.base import BaseEstimator, TransformerMixin
from sklearn.pipeline import Pipeline
from sklearn.impute import SimpleImputer
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression
from sklearn.ensemble import RandomForestClassifier, GradientBoostingClassifier
from sklearn.metrics import roc_auc_score, accuracy_score, f1_score
from xgboost import XGBClassifier
# 1. 模拟数据
X, y = make_classification(
n_samples=1700,
n_features=20,
n_informative=10,
n_redundant=5,
weights=[0.7, 0.3],
class_sep=1.0,
random_state=42
)
X = pd.DataFrame(
X,
columns=[f"feature_{i}" for i in range(X.shape[1])]
)
y = pd.Series(y, name="target")
# 人为加入缺失值
rng = np.random.default_rng(42)
for col in X.columns[:5]:
idx = rng.choice(X.index, size=50, replace=False)
X.loc[idx, col] = np.nan
# 人为制造部分极端值
idx = rng.choice(X.index, size=30, replace=False)
X.loc[idx, "feature_0"] *= 8
# 2. 先划分训练集测试集
X_train, X_test, y_train, y_test = train_test_split(
X,
y,
test_size=0.2,
stratify=y,
random_state=42
)
# 3. 自定义IQR异常值截尾
# 只在训练集fit
class IQRClipper(BaseEstimator, TransformerMixin):
def __init__(self, factor=1.5):
self.factor = factor
def fit(self, X, y=None):
X = pd.DataFrame(X)
self.q1_ = X.quantile(0.25)
self.q3_ = X.quantile(0.75)
self.iqr_ = self.q3_ - self.q1_
self.lower_ = self.q1_ - self.factor * self.iqr_
self.upper_ = self.q3_ + self.factor * self.iqr_
return self
def transform(self, X):
X = pd.DataFrame(X).copy()
return X.clip(
lower=self.lower_,
upper=self.upper_,
axis=1
)
# 4. 数据预处理Pipeline
preprocess = Pipeline([
("imputer", SimpleImputer(strategy="median")),
("outlier", IQRClipper(factor=1.5)),
("scaler", StandardScaler())
])
# 5. 定义模型
models = {
"Logistic Regression":
LogisticRegression(
max_iter=2000,
random_state=42
),
"Random Forest":
RandomForestClassifier(
n_estimators=300,
max_depth=6,
min_samples_leaf=5,
random_state=42
),
"GBDT":
GradientBoostingClassifier(
n_estimators=150,
learning_rate=0.05,
max_depth=3,
random_state=42
),
"XGBoost":
XGBClassifier(
n_estimators=200,
max_depth=3,
learning_rate=0.05,
subsample=0.8,
colsample_bytree=0.8,
eval_metric="logloss",
random_state=42
)
}
# 6. 分层交叉验证
cv = StratifiedKFold(
n_splits=5,
shuffle=True,
random_state=42
)
cv_results = {}
test_results = []
for name, model in models.items():
pipeline = Pipeline([
("preprocess", preprocess),
("model", model)
])
scores = cross_val_score(
pipeline,
X_train,
y_train,
cv=cv,
scoring="roc_auc"
)
cv_results[name] = scores
pipeline.fit(X_train, y_train)
y_prob = pipeline.predict_proba(X_test)[:, 1]
y_pred = pipeline.predict(X_test)
test_results.append({
"Model": name,
"CV_AUC_Mean": scores.mean(),
"CV_AUC_STD": scores.std(),
"Test_AUC": roc_auc_score(
y_test,
y_prob
),
"Accuracy": accuracy_score(
y_test,
y_pred
),
"F1": f1_score(
y_test,
y_pred
)
})
result_df = pd.DataFrame(test_results)
print(result_df.round(4))
# 7. 数据分析图
fig, axes = plt.subplots(
1,
2,
figsize=(15, 6)
)
# 左图:交叉验证AUC分布
axes[0].boxplot(
[cv_results[name] for name in models.keys()],
tick_labels=list(models.keys()),
patch_artist=True,
showmeans=True
)
axes[0].set_title(
"5-Fold Cross Validation ROC-AUC",
fontsize=14
)
axes[0].set_ylabel("ROC-AUC")
axes[0].tick_params(
axis="x",
rotation=20
)
axes[0].grid(
axis="y",
alpha=0.25
)
# 右图:测试集多个指标
plot_df = result_df.set_index("Model")[[
"Test_AUC",
"Accuracy",
"F1"
]]
plot_df.plot(
kind="bar",
ax=axes[1]
)
axes[1].set_title(
"Final Test Set Performance",
fontsize=14
)
axes[1].set_ylabel("Score")
axes[1].set_ylim(0.5, 1.0)
axes[1].tick_params(
axis="x",
rotation=20
)
axes[1].grid(
axis="y",
alpha=0.25
)
axes[1].legend(
loc="lower right"
)
plt.suptitle(
"Model Selection Under Proper Data Preprocessing",
fontsize=17,
fontweight="bold"
)
plt.tight_layout()
plt.show()
这张图我觉得比只画一个Accuracy柱状图更有意义。
左边看的是,不同模型5折交叉验证到底稳不稳定。
如果某个模型箱子很高、波动特别大,说明它非常依赖数据划分。
右边同时看:
AUC
Accuracy
F1
可以避免因为只看一个指标而选错模型。
最后,1700条数据其实完全可以做一个不错的传统机器学习项目。
不用太纠结,到底GBDT还是XGBoost更高级?
更应该把精力放到,数据质量 → 正确划分 → 防止数据泄漏 → 交叉验证 → 多模型Baseline → 调参。
尤其第二个问题一定记住一句话:
可以在划分前做“不依赖数据统计分布”的基础清洗;凡是需要从数据中计算均值、中位数、分位数、标准差、编码规则、特征选择规则的操作,原则上都应该在划分之后,只用训练集去学习。
这一点往往比把XGBoost的 max_depth 从3调成4重要得多。
模型选错一点,可能只是少几个点的指标。
数据泄漏做错了,整个实验结论都可能不可信,这才是最不值得的
最后
我们的学习圈子,不限制 提问内容,学习方法、科研、技术问题、职场等等。
把大家遇到的所有问题都沉淀下来,将会是一套宝贵的资产~
非常欢迎大家加入我们,一起学习,一起分享经验,共同进步!~
以及终身会员,终身学习全部课程以及未来所有的课程 & 答疑:
已经超过1w人加入~
另外,所有课程免费学习,目前已经更新的内容有:
更加详细的介绍,可以看这里,会有更多超级干货带给大家~
随时提问,我都会给大家详细的整理,解决你的问题~
目前已经回答了273个问题,大家感兴趣可以看看,有没有遇到类似的情况,一定会对你有帮助!~

