大数跨境

导师:“标准化和归一化搞懂了吗?”我:“用Codex跑出了完整对比图!”导师:“再看看幂变换和分位数变换,数据预处理可没这么简单!”

导师:“标准化和归一化搞懂了吗?”我:“用Codex跑出了完整对比图!”导师:“再看看幂变换和分位数变换,数据预处理可没这么简单!” 机器学习和人工智能AI
2026-10-15
0

哈喽,大家好~

最近有同学问到:数据没有缺失值,也没有重复值,为什么模型效果还是不稳定?

我觉得是,数据尺度和分布,可能还没处理好

数据清洗解决的是“数据能不能用”,数据转换解决的则是“模型能不能更容易看懂”~

今儿咱们就来聊聊,常见的数据转换方法,看看不同转换方式如何改变数据空间和模型效果。

数据转换

假设一份数据中,收入范围是几千到几十万,而年龄只有十几到几十。如果直接交给模型,收入这个特征很容易因为数值大而占据主导。

数据转换,就是在尽量保留信息的前提下,调整特征的尺度或分布。

常见方法主要有这几类:

  • 标准化:让数据均值接近0、标准差接近1。
  • 归一化:把数据压缩到固定区间,例如 。
  • 幂变换:改善偏态分布,让长尾数据更对称。
  • 分位数变换:根据数据排序,将其映射到均匀分布或正态分布。

像逻辑回归、KNN、SVM和神经网络,对特征尺度都比较敏感。树模型受到的影响相对较小,因为它更关注特征的切分顺序。

也就是,数据转换不是增加新信息,而是把原来的信息换成模型更容易学习的表达方式。

核心原理

最常用的是标准化:

其中, 是原始值, 是特征均值, 是标准差。转换后的数据以0为中心,并具有相近的尺度。

但要注意,标准化只能改变数值大小,不能真正改变分布形状。一个严重右偏的特征,标准化以后依然右偏。

对于正值数据,Box-Cox幂变换可以写成:

控制转换强度。sklearn中的Yeo-Johnson变换原理类似,但它还能处理0和负数,实际使用更加方便。

分位数变换则先估计累计分布 ,再映射到标准正态分布:

这里 是标准正态分布的逆累计分布函数。你可以这样理解:它不太关心原始数值相差多少,而更关心每个样本排在什么位置。

完整案例

我们构造两个严重偏态的特征,并加入少量极端值。模型使用逻辑回归,比较原始数据、标准化、幂变换和分位数变换。

import numpy as np
import matplotlib.pyplot as plt

from sklearn.base import clone
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import (
    FunctionTransformer, StandardScaler,
    PowerTransformer, QuantileTransformer
)
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score, f1_score, roc_auc_score

np.random.seed(42)

# 1. 数据
n = 2200
z = np.random.normal(size=(n, 2))

# 非线性单调变换,制造偏态和尺度差异
X = np.column_stack([
    np.exp(1.2 * z[:, 0]),
    np.sign(z[:, 1]) * np.abs(z[:, 1]) ** 3
])

# 添加少量极端值
idx = np.random.choice(n, int(n * 0.02), replace=False)
X[idx, 0] *= 8
X[idx, 1] *= 4

y = (
    1.2 * z[:, 0] - 0.9 * z[:, 1]
    + 0.35 * np.random.randn(n) > 0
).astype(int)

X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.3, stratify=y, random_state=42
)

transformers = {
    "Raw": FunctionTransformer(),
    "Standard": StandardScaler(),
    "Power": PowerTransformer(method="yeo-johnson"),
    "Quantile": QuantileTransformer(
        n_quantiles=300,
        output_distribution="normal",
        random_state=42
    )
}

colors = ["#00E5FF", "#FF2E93"]
results, fitted = {}, {}

# 2. 训练不同数据转换下的模型
for name, trans in transformers.items():
    t = clone(trans)
    Xtr = t.fit_transform(X_train)
    Xte = t.transform(X_test)

    model = LogisticRegression(C=1.0, max_iter=2000)
    model.fit(Xtr, y_train)

    pred = model.predict(Xte)
    prob = model.predict_proba(Xte)[:, 1]

    results[name] = [
        accuracy_score(y_test, pred),
        f1_score(y_test, pred),
        roc_auc_score(y_test, prob)
    ]
    fitted[name] = (t, model, Xte)

# 3. 图1:转换空间与分类边界
fig, axes = plt.subplots(2, 2, figsize=(12, 9))

for ax, (name, (_, model, Xte)) in zip(axes.flat, fitted.items()):
    # 限制到主要数据区域,避免极端值压缩画面
    x1_min, x1_max = np.percentile(Xte[:, 0], [1, 99])
    x2_min, x2_max = np.percentile(Xte[:, 1], [1, 99])

    xx, yy = np.meshgrid(
        np.linspace(x1_min, x1_max, 220),
        np.linspace(x2_min, x2_max, 220)
    )
    grid = np.c_[xx.ravel(), yy.ravel()]
    zz = model.predict_proba(grid)[:, 1].reshape(xx.shape)

    ax.contourf(
        xx, yy, zz, levels=np.linspace(0, 1, 12),
        cmap="plasma", alpha=0.38
    )
    ax.contour(xx, yy, zz, levels=[0.5],
               colors="#39FF14", linewidths=2.5)

    for label, color in zip([0, 1], colors):
        mask = y_test == label
        ax.scatter(
            Xte[mask, 0], Xte[mask, 1],
            s=17, c=color, alpha=0.65,
            edgecolors="none", label=f"Class {label}"
        )

    ax.set_title(
        f"{name} | F1={results[name][1]:.3f}",
        fontsize=13, fontweight="bold"
    )
    ax.set_xlabel("Feature 1")
    ax.set_ylabel("Feature 2")

axes[0, 0].legend()
fig.suptitle(
    "Feature Transformation and Decision Boundary",
    fontsize=16, fontweight="bold"
)
plt.tight_layout()
plt.show()

# 4. 图2:不同C值的稳定性 + 综合指标
C_values = np.logspace(-3, 2, 10)
score_curve = {}

for name, trans in transformers.items():
    scores = []
    for C in C_values:
        t = clone(trans)
        Xtr = t.fit_transform(X_train)
        Xte = t.transform(X_test)

        model = LogisticRegression(C=C, max_iter=2000)
        model.fit(Xtr, y_train)
        scores.append(f1_score(y_test, model.predict(Xte)))

    score_curve[name] = scores

fig, axes = plt.subplots(1, 2, figsize=(14, 5))
line_colors = ["#FF1744", "#00B0FF", "#76FF03", "#D500F9"]

for (name, scores), color in zip(score_curve.items(), line_colors):
    axes[0].plot(
        C_values, scores, marker="o",
        linewidth=2.5, color=color, label=name
    )

axes[0].set_xscale("log")
axes[0].set_xlabel("Regularization parameter C")
axes[0].set_ylabel("F1 score")
axes[0].set_title("Model Stability under Different C")
axes[0].legend()

names = list(results.keys())
metrics = np.array(list(results.values()))
x = np.arange(len(names))
width = 0.23
metric_names = ["Accuracy", "F1", "ROC-AUC"]
bar_colors = ["#00E5FF", "#FF4081", "#76FF03"]

for i in range(3):
    axes[1].bar(
        x + (i - 1) * width,
        metrics[:, i],
        width,
        label=metric_names[i],
        color=bar_colors[i]
    )

axes[1].set_xticks(x)
axes[1].set_xticklabels(names)
axes[1].set_ylim(0.5, 1.0)
axes[1].set_title("Overall Model Performance")
axes[1].legend()

plt.tight_layout()
plt.show()

print("Accuracy / F1 / ROC-AUC")
for name, value in results.items():
    print(name, np.round(value, 3))

第一张图的重点是观察绿色分类边界与两类样本的关系。

原始数据中,极端值会拉伸坐标空间。标准化统一了尺度,但没有消除偏态;幂变换和分位数变换能够重新整理数据形状,让潜在关系更接近线性,因此逻辑回归更容易找到有效边界。

第二张图左侧展示不同正则化参数 下的F1变化。

曲线越高、越平稳,说明模型对参数越不敏感。右侧同时比较Accuracy、F1和ROC-AUC,避免只靠一个指标下结论。

实际项目中,转换器必须只在训练集上执行fit,再用同一个转换器处理验证集和测试集。否则测试集信息会提前进入训练过程,造成数据泄漏。

总结

数据转换的核心,不是机械地把所有特征都变成正态分布,而是让特征尺度、分布形状和模型假设更加匹配。标准化解决尺度问题,幂变换处理偏态,分位数变换对长尾和极端值更有优势。

接下来大家可以把逻辑回归换成KNN或SVM,观察距离模型对转换方式的敏感程度;也可以配合交叉验证,把转换方法作为Pipeline的一部分统一调参。总之就是说,先把数据表达处理好,再调模型,往往会省下不少时间~

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