大数跨境

通透!归一化方法 !!

通透!归一化方法 !! 机器学习和人工智能AI
2026-11-03
3

哈喽,大家好~

今儿,咱们来聊聊,归一化方法中分位数变换的逻辑~

之前有人提过,特征偏得厉害,StandardScaler 处理完还是不好用,怎么办?

可以试试分位数变换~

什么是分位数变换

普通标准化关注的是均值和标准差:

问题在于,均值和标准差很容易被极端值拉偏。

比如大部分用户消费金额在 100 元左右,少数用户消费 10 万元。经过标准化以后,大量普通用户的数据会被挤在很小的范围内。

分位数变换换了一个思路:不太关心原始数值有多大,而是关注它排在什么位置。

假设数据为:

数字 1000 虽然特别大,但从排序角度看,它只是排在第 100% 分位。这样一来,极端值的影响就被限制住了。

核心原理

分位数变换通常分为两步。

第一步,使用经验累积分布函数,把原始数据映射为分位数:

其中,  表示小于等于 的数据比例, 位于 之间。

第二步,根据目标分布继续映射:

是目标分布的逆累积分布函数。如果目标是均匀分布,直接保留 ;如果目标是标准正态分布,就使用正态分布的逆函数。

一句话概括:先把数值换成排名,再把排名放进指定分布。

它是一种非线性变换,但不会改变样本在单个特征上的大小顺序。

完整案例

我们构造一个二分类数据集,再对特征进行指数、三次方变换,并加入少量异常值。这样更接近业务中常见的偏态数据。

下面同时完成两组分析:

  • 对比变换前后的分类空间和决策边界;
  • 逐渐增加异常值比例,比较标准化与分位数变换的稳定性。
import numpy as np
import matplotlib.pyplot as plt

from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler, QuantileTransformer
from sklearn.linear_model import LogisticRegression
from sklearn.pipeline import make_pipeline
from sklearn.metrics import accuracy_score


# 构造带偏态和异常值的数据
def make_data(n=1600, outlier_rate=0.05, seed=42):
    rng = np.random.default_rng(seed)

    # 潜在特征原本接近正态分布
    Z = rng.normal(size=(n, 2))
    score = Z[:, 0] + 0.8 * Z[:, 1] + rng.normal(0, 0.55, n)
    y = (score > 0).astype(int)

    # 使用非线性变换制造明显偏态
    X = np.empty_like(Z)
    X[:, 0] = np.sign(Z[:, 0]) * np.expm1(1.5 * np.abs(Z[:, 0]))
    X[:, 1] = Z[:, 1] ** 3

    # 加入随机极端值
    k = int(n * outlier_rate)
    idx = rng.choice(n, k, replace=False)
    X[idx] += rng.normal(0, 35, size=(k, 2))

    return X, y


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

# 模型1:标准化 + 逻辑回归
standard_model = make_pipeline(
    StandardScaler(),
    LogisticRegression(max_iter=2000)
)
standard_model.fit(X_train, y_train)

# 模型2:分位数变换 + 逻辑回归
qt = QuantileTransformer(
    n_quantiles=300,
    output_distribution="normal",
    random_state=42
)
X_train_q = qt.fit_transform(X_train)
X_test_q = qt.transform(X_test)

quantile_model = LogisticRegression(max_iter=2000)
quantile_model.fit(X_train_q, y_train)

acc_std = accuracy_score(y_test, standard_model.predict(X_test))
acc_qt = accuracy_score(y_test, quantile_model.predict(X_test_q))

# 绘制决策空间
def draw_boundary(ax, X_plot, y_plot, model, title):
    x1_min, x1_max = np.percentile(X_plot[:, 0], [1, 99])
    x2_min, x2_max = np.percentile(X_plot[:, 1], [1, 99])

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

    ax.contourf(
        xx, yy, prob,
        levels=np.linspace(0, 1, 12),
        cmap="cool",
        alpha=0.65
    )
    ax.contour(xx, yy, prob, levels=[0.5],
               colors="#FFF200", linewidths=2.5)

    ax.scatter(
        X_plot[:, 0], X_plot[:, 1],
        c=y_plot, cmap="spring",
        s=22, edgecolors="white", linewidths=0.25, alpha=0.8
    )
    ax.set_title(title, fontsize=13, fontweight="bold")
    ax.set_xlabel("Feature 1")
    ax.set_ylabel("Feature 2")


fig, axes = plt.subplots(1, 2, figsize=(13, 5))

draw_boundary(
    axes[0], X_test, y_test, standard_model,
    f"StandardScaler + LR | Accuracy={acc_std:.3f}"
)
draw_boundary(
    axes[1], X_test_q, y_test, quantile_model,
    f"QuantileTransformer + LR | Accuracy={acc_qt:.3f}"
)

plt.suptitle(
    "Decision Space Before and After Quantile Transform",
    color="#00F5FF", fontsize=16, fontweight="bold"
)
plt.tight_layout()
plt.show()

# 分析不同异常值比例下的稳定性
rates = np.linspace(0, 0.15, 7)
std_scores, qt_scores = [], []

for rate in rates:
    one_std, one_qt = [], []

    for seed in range(8):
        X, y = make_data(outlier_rate=rate, seed=seed)
        Xtr, Xte, ytr, yte = train_test_split(
            X, y, test_size=0.3, stratify=y, random_state=seed
        )

        m1 = make_pipeline(
            StandardScaler(),
            LogisticRegression(max_iter=2000)
        )
        m2 = make_pipeline(
            QuantileTransformer(
                n_quantiles=300,
                output_distribution="normal",
                random_state=seed
            ),
            LogisticRegression(max_iter=2000)
        )

        m1.fit(Xtr, ytr)
        m2.fit(Xtr, ytr)

        one_std.append(m1.score(Xte, yte))
        one_qt.append(m2.score(Xte, yte))

    std_scores.append(one_std)
    qt_scores.append(one_qt)

std_scores = np.array(std_scores)
qt_scores = np.array(qt_scores)

plt.figure(figsize=(9, 5))

for scores, color, label in [
    (std_scores, "#FF2D95", "StandardScaler"),
    (qt_scores, "#00E5FF", "QuantileTransformer")
]:
    mean = scores.mean(axis=1)
    std = scores.std(axis=1)

    plt.plot(rates, mean, marker="o", linewidth=3,
             color=color, label=label)
    plt.fill_between(rates, mean - std, mean + std,
                     color=color, alpha=0.18)

plt.xlabel("Outlier Rate")
plt.ylabel("Test Accuracy")
plt.title("Model Robustness Under Increasing Outliers",
          fontsize=15, fontweight="bold")
plt.grid(alpha=0.2)
plt.legend()
plt.tight_layout()
plt.show()

第一张图展示的是联合特征空间和模型决策边界。左侧原始空间被偏态和极端值拉伸,普通样本容易挤在一起;右侧经过分位数变换后,样本展开得更均匀,逻辑回归更容易找到有效边界。

第二张图不是简单看一次准确率,而是逐步提高异常值比例,并在多个随机数据集上重复实验。实线表示平均准确率,阴影表示波动范围。

如果青色曲线下降更慢,说明分位数变换面对极端值更加稳定。总之就是说,它不是让异常值消失,而是限制异常值对尺度和模型参数的支配能力。

注意点

output_distribution="normal"适合逻辑回归、神经网络等偏好规则尺度的模型;设置为"uniform"则会把特征映射到 。

n_quantiles控制经验分位点数量。数值越大,映射越细,但不能超过训练样本数。实际使用时必须只在训练集上fit,否则会发生数据泄漏。

另外,分位数变换会改变样本间的原始距离,也会压缩极端值信息。如果异常高金额本身具有明确业务意义,就不能不加判断地使用。对于决策树这类基于排序切分的模型,收益通常也没有线性模型明显。

总结

分位数变换的核心,就是把“原始数值”换成“数据排名”,再映射到均匀分布或正态分布。它特别适合偏态明显、异常值较多,同时又依赖特征尺度的模型。

接下来大家可以继续尝试对比RobustScaler,看看谁在不同异常值比例下更稳;也可以把逻辑回归换成KNN或神经网络,观察距离结构变化以后,模型效果会发生什么变化~

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