大数跨境

导师:“GBDT、XGBoost、随机森林,选哪个?”我:“当然选最强的!”导师笑了:“1700条样本,你最该担心的根本不是模型!”

导师:“GBDT、XGBoost、随机森林,选哪个?”我:“当然选最强的!”导师笑了:“1700条样本,你最该担心的根本不是模型!” 机器学习和人工智能AI
2026-09-04
1

很多同学做机器学习的时候,很多同学都遇到这样的困惑,这次用随机森林、GBDT,还是直接上XGBoost?

但实际项目里,真正容易出问题的,往往不是模型选得不够高级,而是前面的数据处理顺序没搞对。

最后AUC、Accuracy看着很好,结果却未必可信。

所以我一直比较建议大家:先把数据处理流程理清楚,再去纠结模型。

最近刚好有同学问到这样的问题,咱们一起来看下,也欢迎大家加入我们的学习圈子~

个问题很典型,下面就借着这段对话,把这几个容易踩坑的地方聊清楚~

同学提问

原文:https://www.coszhuang.cn/questions/871

主题:机器学习方法的选择、以及数据异常值的处理

  1. 我一共有1700个样本,分类问题进行机器学习,GBDT、XGBoost或其他方法哪个更适合呢?
  2. 收集完数据之后,需要进行异常值和缺失值的处理,这两部都需要在划分训练集和测试集之前进行吗?请问划分训练集和测试集之前需要做什么呢?

大壮回复

事实上很多同学,尤其是刚开始做机器学习项目的时候,很容易把注意力全部放在:我到底用 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.70.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=(156)
)


# 左图:交叉验证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.51.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个问题,大家感兴趣可以看看,有没有遇到类似的情况,一定会对你有帮助!~


针对各个方面,都为大家提供了详细的回复~
总有能够帮助到大家的方面!~

【声明】内容源于网络
0
0
机器学习和人工智能AI
让我们一起期待 AI 带给我们的每一场变革!推送最新行业内最新最前沿人工智能技术!
内容 381
粉丝 0
机器学习和人工智能AI 让我们一起期待 AI 带给我们的每一场变革!推送最新行业内最新最前沿人工智能技术!
总阅读5.0k
粉丝0
内容381