跳到正文
博客园· wang_yb·· 4 小时前AI 评分36

MinMax vs Standard vs Robust:三种特征缩放怎么选

MinMax vs Standard vs Robust:三种特征缩放怎么选 - wang_yb

AI 导读

用一份含 0.3% 团购大单、偏度 2.07 的合成订单金额数据,对比 MinMax、Standard、Robust 三种特征缩放器的实际效果。文章指出缩放只做平移和拉伸、不改变分布形状,差别在于用哪两个数当尺子,并给出一套按异常值占比和偏度排序的选型规则。

正文

做特征工程时很多人有一套固定动作:StandardScaler 然后 fit_transform。(也就是先标准化数据,在训练模型)

这在教科书数据集上没问题,但真实数据,比如订单金额、页面停留时长、接口耗时等,几乎天然右偏,还总混着几个离谱的值。

StandardScaler 拿均值和标准差当尺子,而这两个数本身会被极端值带偏。你以为做了标准化,其实是把正常样本的区分度压小了。

本文用一份订单金额数据,把三种常用的特征缩放器(MinMax vs Standard vs Robust)挨个跑一遍,看它们各自把数据变成了什么样,最后给一套能直接抄进项目的选型规则。

1. 缩放只做两件事

先说清楚一件事:缩放不改变分布形状,右偏还是右偏,长尾还是长尾。它只做平移(改位置)和拉伸(改尺度)。

那为什么还要缩放?因为模型看的是数字,不是单位。

一张表里同时放着订单金额(0~200)和下单件数(1~5),对 KNN、SVM、带正则项的线性模型、神经网络来说,金额这个特征天然"嗓门大"。

缩放就是让所有特征站到同一条起跑线上。

既然只是平移加拉伸,三种缩放器的全部差别就只剩一个问题:用哪两个数当尺子。

结论已经藏在图里了。

  1. MinMax 的两根参考线在数据的两端,多一笔大单它就得多挪一大截;
  2. Standard 的均值和标准差是拿全部样本算的,极端值当然算在内;
  3. Robust 的两根线取自中间那一半数据,右边发生什么都不影响它。

一句话:谁离极端值越近,谁越容易被带偏。

2. 实战:一份订单金额数据

用合成数据,参数可控、结果可复现。

模拟一家店的订单金额:800 笔普通零售单(集中在 50 元附近)+ 800 笔小额长尾单(凑单、活动价,天然右偏)+ 5 笔 180~210 元的团购单——占比只有 0.3%,金额却是正常单的三四倍。

np.random.seed(42)
normal_orders = np.random.normal(50, 15, 800)          # 普通零售单
long_tail = np.random.exponential(2, 800) * 10 + 20    # 小额长尾单
amount = np.concatenate([normal_orders, long_tail, [200, 180, 190, 210, 195]])

缩放之前先体检,这一步不能省:

x = df["amount"]
print(f"均值 {x.mean():.2f} 中位数 {x.median():.2f} 标准差 {x.std():.2f}")
print(f"偏度 {stats.skew(x):.2f} 范围 {x.min():.1f} ~ {x.max():.1f}")
均值 45.65 中位数 42.81 标准差 20.52
偏度 2.07 范围 1.4 ~ 210.0

这几个数要连起来读。

均值大于中位数,说明右边拖了条长尾巴;

偏度 2.07 把这件事量化了(远大于 1 就是强右偏);

范围里藏着第三个信息,即最大值 210 比绝大多数订单高出一个数量级,就是那 5 笔团购单。

3. StandardScaler

先跑最常用的那个。

办法很直接:把"含异常值"和"剔除异常值(<150 元)"各算一遍,看参考值漂了多少。

mean_all, std_all = df["amount"].mean(), df["amount"].std()
clean = df[df["amount"] < 150]["amount"]
print(f"含异常值 std={std_all:.2f} 剔除后 std={clean.std():.2f}")
print(f"50 元订单 z-score: {(50 - mean_all) / std_all:.2f} "
      f"vs {(50 - clean.mean()) / clean.std():.2f}")
含异常值 std=20.52 剔除后 std=18.51
50 元订单 z-score: 0.21 vs 0.26

均值只动了 0.54,看不出什么;标准差从 18.51 涨到 20.52,涨了 10.86%——这个才要紧,因为 z-score 的分母就是它。

分母变大,所有正常订单的 z-score 被整体压扁:一笔 50 元的典型订单,本来该落在 0.26 个标准差的位置,现在只剩 0.21。

同一组异常值,对标准差(StandardScaler)的杀伤力是对 IQR(RobustScaler) 的十倍以上。

差别在于:标准差的计算里每个样本都要跟均值做差再平方,一个离得特别远的点,贡献是平方级的;IQR 只问第 25 和 75 百分位在哪,极端值连投票权都没有。

说句公道话,它的问题只在"偏态 + 异常值"同时出现时才明显。

用距离或内积的算法——KNN、SVM-RBF、KMeans、PCA、带正则的线性模型、神经网络等等会受直接影响;树模型(XGBoost、LightGBM、随机森林)根本不看尺度,这一步可以整个跳过。

4. MinMaxScaler

MinMaxScaler 最好解释:最小值映射成 0,最大值映射成 1,中间线性铺开。

问题是这个"最大值"掌握在极端值手里。

df["minmax"] = MinMaxScaler().fit_transform(df[["amount"]]).ravel()
for p in [50, 75, 90, 95, 99]:
    print(f"{p}% 的订单落在 {df['minmax'].quantile(p / 100):.3f} 以下")
print(f"{(df['minmax'] < 0.5).mean() * 100:.1f}% 被塞进 0 ~ 0.5")
50% 的订单落在 0.199 以下
75% 的订单落在 0.262 以下
90% 的订单落在 0.319 以下
95% 的订单落在 0.368 以下
99% 的订单落在 0.541 以下
98.6% 被塞进 0 ~ 0.5

98.6% 的订单挤在 0~0.5,剩下 57% 的取值空间只服务那 5 笔团购单。

缩放是线性的,形状一点没变,变的是正常订单能分到多少空间。

要害不在精度,在分辨率:20 元和 90 元的订单差了四倍多,缩放后只差 0.335。

这个差距在神经网络第一层输入上尤其吃亏,KNN 这类靠距离说话的算法也会把绝大多数样本挤成一团。

所以它的适用条件很窄:字段有天然边界,且这个边界本身有意义(评分 1~5、百分比、像素 0~255),并且确认没有异常值。

5. RobustScaler

公式是 (x - median) / IQR。理解 IQR 最好的方式是看箱线图。

箱体里装的是中间 50% 的样本,中位数是箱体里那根粗线,IQR 就是箱体的宽度。

判定异常值的经典规则也出自这里:超过 Q3 + 1.5 × IQR 算异常。

本例上限约 93 元,超出它的订单占 2.0%。注意这个比例是按"位置"算的,跟具体有多极端无关,一笔 94 元和一笔 210 元权重一样。

这就是它稳的原因。

df["robust"] = RobustScaler().fit_transform(df[["amount"]]).ravel()
median_all = df["amount"].median()
iqr_all = df["amount"].quantile(0.75) - df["amount"].quantile(0.25)
clean = df[df["amount"] < 150]["amount"]
iqr_clean = clean.quantile(0.75) - clean.quantile(0.25)
print(f"含异常值 median={median_all:.2f} IQR={iqr_all:.2f}")
print(f"剔除后   median={clean.median():.2f} IQR={iqr_clean:.2f}")
print(f"50 元订单 robust score: {(50 - median_all) / iqr_all:.2f} "
      f"vs {(50 - clean.median()) / iqr_clean:.2f}")
含异常值 median=42.81 IQR=25.31
剔除后   median=42.80 IQR=25.08
50 元订单 robust score: 0.28 vs 0.29

中位数漂移 0.02%,IQR 漂移 0.96%,同一笔订单缩放结果 0.28 对 0.29,基本没变。

RobustScaler 的代价也得说清楚:输出没有边界。

MinMax 死死卡在 0~1,Standard 大约落在 -2.16~8.01,Robust 大约落在 -1.64~6.61。

此外,它的下游要是需要有界输入,得在它后面再接一层。

可解释性也差一点,比如 "0.28 个 IQR"不如"0.28 个标准差"直觉。

6. 选型:三轮判断

参考值 输出范围 怕什么 适用场景
MinMaxScaler min / max 固定在 [0,1] 极端值撑大分母 有天然边界、确认无异常值
StandardScaler mean / std 无界 偏态 + 异常值 近似正态、需要可解释性
RobustScaler median / IQR 无界 基本不怕 偏态、异常值不能删

def recommend_scaler(data):
    skewness = abs(stats.skew(data))
    q25, q75 = np.percentile(data, [25, 75])
    iqr = q75 - q25
    outlier_pct = ((data > q75 + 1.5 * iqr) | (data < q25 - 1.5 * iqr)).mean() * 100
    if outlier_pct > 5:
        return "RobustScaler - 异常值占比过高"
    if skewness > 1:
        return "RobustScaler - 强偏态分布"
    if skewness < 0.5 and outlier_pct < 1:
        return "StandardScaler - 近似正态分布"
    return "RobustScaler - 默认安全牌"
偏度 2.07 异常值占比 2.0%
推荐结果: RobustScaler - 强偏态分布

三种特征缩放的使用要点:

  1. 优先级不能反:先看异常值占比,再看偏度,因为异常值的破坏更直接。
  2. 阈值是经验值:你的业务如果天然异常值多,可以把 5% 调大。
  3. 它是起点不是终点:最终还是拿验证集说话,把两三个候选都跑一遍对比,比任何启发式都靠谱。

另外,特征缩放也可以按列做——一张表里金额偏态、件数正态,各列用各自的缩放器。

7. 完整代码

完整可运行代码共享在:order_amount_scaler_demo.py: https://url11.ctfile.com/f/45455611-17569904272488-59d961?p=6872 (访问密码: 6872)

感兴趣的话最好自己手动跑一遍感受一下。

python order_amount_scaler_demo.py

最后在补充一句:缩放器本身没有高下之分,区别在于数据长什么样。

有天然边界且干净,MinMax 最好用也最好解释;

近似正态、需要跟人解释,Standard 更合适;

真实业务里那些又偏又脏、还混着几笔大单的字段,RobustScaler 是最省心的默认选择。

来源:博客园 · cnblogs.com