问题、先修与学习目标
特征工程不是将数据机械地『变得好看』,而是把原始观测转换为与决策问题相容、可被模型识别的信息。在 A 股截面中,市值的单位可能是亿元,股息率是小数,市盈率还可能由于亏损而为负。如果不先处理尺度、极端值、类别与信息时点,模型的数学目标与金融问题就会脱节。
本章先修是线性代数中的内积、特征值与正交基,概率统计中的均值、方差和分位数,以及 Pandas 的分组和时间索引。学完后,读者应能够:
推导标准化、Min–Max 缩放、分位数缩尾与 PCA 的核心公式;
比较这些变换的不变性、异常值敏感性和适用边界;
在标签可得日约束下实现平滑的时间 OOF 目标编码,并说明公司分组隔离与时间隔离的差异;
从载荷和解释方差比解读主成分,而不只是调用 PCA.fit_transform();
识别预处理泄漏和类别不均衡,选择与业务损失相匹配的评价指标。
本章用下表明确正式目标的活动与评价证据。
表 表 15.1 汇总本节的计算或审计结果,解释时应遵循正文给出的口径与限制。
真实案例与时点安全样本
本章使用本地 valuation_factors_quarterly_15_years.h5 和 stock_basic_data.h5。估值文件的主键是『证券—日期』两层索引,但文件是 HDF5 fixed 布局,不支持 where 与列投影。因此 列表 15.1 必须先读取该单表,紧接着只保留所需列和期间。这是数据布局的限制,不是选择性读取的理想状态。
为避免把其他地区公司冒充为长三角样本,列表 15.2 依基础表的省份字段显式筛选上海、江苏、浙江和安徽。每一行显式保留特征观测日 feature_date、结果观测日 target_date 与标签可得日 label_available_date。市值是当日市场观测,因此本例取 label_available_date = target_date;若目标来自财报,则应改用实际披露日。任务仍称『下一季度』,所以只保留两个观测日的自然月差严格等于 3 的配对,不能把缺数造成的 6 个月或 15 个月跨度混入同一标签。每个特征日期下,市值变化率位于同期截面前五分位的公司被标记为正类;这个定义形成约 20% 的不均衡分类任务,但『截面前五分位』只是排序标签,不是统计显著性结论。
易混淆概念辨析:市值变化率不等于股票收益率
这里的比率由相邻观测季度的总市值计算。总市值同时受价格、总股本以及送转、增发、回购等公司行动影响,也未计入现金分红;当前数据又没有逐期股份数量与公司行动调整链。因此本章只把它称为『下一季度市值变化率』,不能解释为个股持有期收益、相对基准超额收益或投资者财富增长。
表 15.2 先展示原始相邻观测的跨度分布,再标明只有三个月且市值变化率可计算的样本进入目标面板。这样可以区分『下一次可观察季度』和口径固定的『下一季度』,也不会把目标市值缺失静默混入保留数。
prediction_horizon_audit['进入目标面板数'] = 0 # 先把非三个月跨度明确记录为未采用
prediction_horizon_audit.loc[3, '进入目标面板数'] = len(feature_panel) # 记录三个月且市值变化率可计算的样本
prediction_horizon_audit['排除数'] = prediction_horizon_audit['筛选前样本数'].sub(prediction_horizon_audit['进入目标面板数']) # 量化跨度或目标缺失造成的剔除规模
assert feature_panel['horizon_months'].eq(3).all() # 防止不规则季度缺口重新进入下一季度任务
prediction_horizon_audit # 展示三个月与其他跨度的保留和剔除数量
时间留出和变换的信息集
设信息集 \(ℐ_t\) 包含决策时点 \(t\) 之前已经形成的特征和标签。对任意拟合型变换 \(T_{θ}\),安全条件不仅是训练行的 feature_date 早于测试日,还要求每条拟合标签满足 label_available_date < t。测试集首个信息时点是 2025 年第一个有效季度末;特征日在此前、但标签到该日才形成的边界样本必须 purge。分割规模、purge 数量与非法标签断言见 表 15.3。
numeric_features = ['pe_ratio_ttm', 'pb_ratio_lf', 'ps_ratio_ttm', 'dividend_yield_ttm', 'log_market_cap'] # 统一数值特征顺序
required_features = numeric_features + ['industry', 'order_book_id', 'feature_date', 'target_date', 'label_available_date', 'is_top_quintile'] # 保留建模字段和三类时点
model_panel = feature_panel.dropna(subset=numeric_features).loc[:, required_features].sort_values(['feature_date', 'order_book_id']).reset_index(drop=True) # 获得可复现且保留标签血缘的建模样本
test_information_start = model_panel.loc[model_panel['feature_date'] >= pd.Timestamp('2025-01-01'), 'feature_date'].min() # 以首个2025年有效截面定义测试决策时点
assert pd.notna(test_information_start) # 保证本地数据确实提供可评估的未来留出期
is_historical_feature = model_panel['feature_date'] < test_information_start # 标识测试时点前已经形成的候选特征
is_label_known_before_test = model_panel['label_available_date'] < test_information_start # 标识测试决策前严格可得的结果
is_training_row = is_historical_feature & is_label_known_before_test # 按标签可得日 purge 最终训练集
is_test_row = model_panel['feature_date'] >= test_information_start # 保留测试时点及之后的统一期限样本
purged_holdout_boundary_count = int((is_historical_feature & ~is_label_known_before_test).sum()) # 统计因结果尚不可得而延迟进入训练的行
training_panel = model_panel.loc[is_training_row].copy() # 只保留测试时点前已知标签的历史样本
test_panel = model_panel.loc[is_test_row].copy() # 保留未来特征及其事后评价标签
holdout_illegal_label_count = int(training_panel['label_available_date'].ge(test_information_start).sum()) # 计算最终拟合端的非法标签数
assert holdout_illegal_label_count == 0 # 用可执行条件阻止测试期结果进入最终训练
split_audit = pd.DataFrame({'特征起始日': [training_panel['feature_date'].min(), test_panel['feature_date'].min()], '特征结束日': [training_panel['feature_date'].max(), test_panel['feature_date'].max()], '最大标签可得日': [training_panel['label_available_date'].max(), test_panel['label_available_date'].max()], '样本数': [len(training_panel), len(test_panel)], '边界 purge 数': [purged_holdout_boundary_count, 0], '非法训练标签数': [holdout_illegal_label_count, 0], '正类比例': [training_panel['is_top_quintile'].mean(), test_panel['is_top_quintile'].mean()]}, index=['最终训练集', '时间留出集']) # 同时审计时点、规模、标签边界与不均衡程度
split_audit # 展示最终训练和留出样本的可执行审计证据
类别特征与无泄漏目标编码
One-Hot 编码将 \(K\) 个类别映射为 \(K\) 个稀疏指示变量。它不强加类别顺序,适合低至中等基数。高基数时,特征维数和内存会迅速增长,这时才有理由考虑目标编码。
对类别 \(c\),训练子集中有 \(n_c\) 个观测、目标均值为 \(\bar y_c\),全局均值为 \(\bar y\)。平滑目标编码定义为
\[
\operatorname{TE}_{\alpha}(c)=\frac{n_c\bar y_c+\alpha\bar y}{n_c+\alpha},\qquad \alpha>0.
\tag{15.4}\]
式 式 15.4 用 \(\alpha\) 控制类别均值向全局均值收缩的强度。当 \(n_c→∞\) 时编码趋近类别均值;当 \(n_c\) 很小时,它被收缩到全局均值。然而,平滑并不自动消除泄漏。如果第 \(i\) 行的 \(y_i\) 参与了自己的编码,特征就已间接『看见』答案。
列表 15.3 封装平滑规则。主流程按验证信息日做扩展窗口 OOF:每折先取特征日更早的候选行,再 purge 其中 label_available_date 不早于验证日的标签。表 15.5 对每折报告候选数、purge 数和非法标签数;测试映射只由最终训练集中在首个测试信息时点前已经可得的标签估计。未见行业回退到同一安全拟合集的全局均值。
encoding_validation_dates = np.sort(training_panel['feature_date'].unique())[-3:] # 选择历史末端三个截面演示时间 OOF
training_oof_encoding = pd.Series(index=training_panel.index, dtype='float64') # 预留与原训练索引一致的 OOF 容器
encoding_fold_rows = [] # 收集折级标签可得性证据
for validation_information_time in encoding_validation_dates: # 逐个历史决策时点重估类别映射
is_fit_candidate = training_panel['feature_date'] < validation_information_time # 先建立特征日在验证日前的候选拟合集
is_fit_label_known = training_panel['label_available_date'] < validation_information_time # 再要求候选标签当时已经可得
fold_fit = training_panel.loc[is_fit_candidate & is_fit_label_known] # purge 尚未形成结果的历史特征行
fold_validation = training_panel.loc[training_panel['feature_date'] == validation_information_time] # 保持完整同期截面作为 OOF 验证集
illegal_label_count = int(fold_fit['label_available_date'].ge(validation_information_time).sum()) # 计算拟合端的非法未来标签数
assert illegal_label_count == 0 and len(fold_fit) > 0 # 要求每折有可用历史且非法标签严格为零
fold_mapping, fold_prior = fit_smoothed_target_mapping(fold_fit['industry'], fold_fit['is_top_quintile'], 20.0) # 仅从当时已知标签学习映射
training_oof_encoding.loc[fold_validation.index] = fold_validation['industry'].map(fold_mapping).fillna(fold_prior) # 对当前未来截面生成 OOF 编码
purged_label_count = int((is_fit_candidate & ~is_fit_label_known).sum()) # 量化仅比较特征日会误纳入的标签
encoding_fold_rows.append({'验证信息日': validation_information_time, '候选拟合数': int(is_fit_candidate.sum()), '标签 purge 数': purged_label_count, '最终拟合数': len(fold_fit), '非法标签数': illegal_label_count, 'OOF 编码数': len(fold_validation)}) # 保存折级审计
assert training_panel['label_available_date'].lt(test_information_start).all() # 最终测试映射不得吸收测试时点才形成的标签
full_mapping, training_prior = fit_smoothed_target_mapping(training_panel['industry'], training_panel['is_top_quintile'], 20.0) # 用安全最终训练集估计测试映射
test_target_encoding = test_panel['industry'].map(full_mapping).fillna(training_prior) # 不使用任何留出期目标修正映射
encoding_fold_audit = pd.DataFrame(encoding_fold_rows) # 形成可执行的折级审计表
assert encoding_fold_audit['非法标签数'].eq(0).all() # 防止未来改动放宽标签可得日条件
encoding_fold_audit # 展示每折 purge 后非法标签数均为零
易混淆概念辨析:公司隔离不等于时间隔离
GroupKFold 防止同一公司跨折,却不能单独模拟逐季上线。表 15.6 因而只做公司交集检查,不参与上面的时间 OOF 编码,也不为测试集学习映射。示范输入仍限定为最终安全训练集,所以不会吸收首个测试信息时点尚不可得的标签。
from sklearn.model_selection import GroupKFold # 将同一公司全部历史观测绑定到同一侧
group_splitter = GroupKFold(n_splits=5) # 建立公司隔离示范而非时间回测
group_isolation_rows = [] # 收集公司交集和测试时点标签边界
for fold_number, (fit_positions, validation_positions) in enumerate(group_splitter.split(training_panel, training_panel['is_top_quintile'], groups=training_panel['order_book_id']), start=1): # 逐折检查公司集合
fit_companies = set(training_panel.iloc[fit_positions]['order_book_id']) # 提取公司隔离拟合侧
validation_companies = set(training_panel.iloc[validation_positions]['order_book_id']) # 提取公司隔离验证侧
company_overlap_count = len(fit_companies.intersection(validation_companies)) # 计算同一公司跨侧数量
unavailable_label_count = int(training_panel.iloc[fit_positions]['label_available_date'].ge(test_information_start).sum()) # 检查是否吸收测试边界未来标签
group_isolation_rows.append({'折': fold_number, '公司交集数': company_overlap_count, '测试时点不可得标签数': unavailable_label_count}) # 保存教学示范证据
group_isolation_audit = pd.DataFrame(group_isolation_rows) # 汇总五折隔离结果
assert group_isolation_audit[['公司交集数', '测试时点不可得标签数']].eq(0).all().all() # 同时阻止公司重叠和未来标签
group_isolation_audit # 展示 GroupKFold 的有限用途和安全输入边界
PCA:从最大方差到载荷解释
中心化、协方差与特征分解
设缩尾并标准化后的训练矩阵为 \(Z∈ℝ^{n×p}\),每列样本均值为 0。其样本协方差矩阵为
\[
S=\frac{1}{n-1}Z^{\mathsf T}Z.
\tag{15.5}\]
式 式 15.5 使用样本自由度 \(n-1\) 估计标准化特征之间的协方差。第一主成分寻找单位向量 \(w_1\) 使投影方差最大,如 式 15.6 所示:
\[
\max_{w_1} \operatorname{Var}(Zw_1)
=\max_{w_1}w_1^{\mathsf T}Sw_1,
\qquad \text{s.t. }w_1^{\mathsf T}w_1=1.
\tag{15.6}\]
构造拉格朗日函数 \(L(w,λ)=w^{\mathsf T}Sw-λ(w^{\mathsf T}w-1)\),对 \(w\) 求导可得 式 15.7:
\[
\frac{\partial L}{\partial w}=2Sw-2\lambda w=0
\quad\Longrightarrow\quad Sw=\lambda w.
\tag{15.7}\]
因此最优方向是 \(S\) 最大特征值对应的特征向量。后续主成分在与先前方向正交的约束下递归求解。若 \(λ_1≥…≥λ_p≥0\),前 \(k\) 个主成分的累计解释方差比由 式 15.8 给出:
\[
R_k=\frac{\sum_{j=1}^{k}\lambda_j}{\sum_{j=1}^{p}\lambda_j}.
\tag{15.8}\]
载荷 \(w_{mj}\) 是特征 \(m\) 在第 \(j\) 个主成分方向上的系数,得分则是 \(t_{ij}=z_i^{\mathsf T}w_j\)。特征向量的正负号可整体翻转,因而不应把单个载荷的符号解读为稳定的因果方向。表 15.7 与 表 15.8 分别展示载荷和完整方差分解。
from sklearn.decomposition import PCA # 使用 SVD 稳定求解主成分
from sklearn.preprocessing import StandardScaler # 消除估值比率与市值的量纲差异
pca_scaler = StandardScaler() # 创建只在训练期拟合的尺度变换
training_standardized = pca_scaler.fit_transform(training_winsorized) # 估计并应用训练均值与标准差
test_standardized = pca_scaler.transform(test_winsorized) # 使用历史参数变换未来数据
pca_model = PCA() # 保留所有成分以审计完整方差分解
training_scores = pca_model.fit_transform(training_standardized) # 仅在训练期学习正交方向
test_scores = pca_model.transform(test_standardized) # 将测试期投影到固定的历史基底
loading_table = pd.DataFrame(pca_model.components_.T, index=numeric_features, columns=[f'PC{component_number}' for component_number in range(1, len(numeric_features) + 1)]) # 将特征向量按原始特征排列
loading_table.round(3) # 展示各主成分的载荷结构
sample_covariance = np.cov(training_standardized, rowvar=False, ddof=1) # 按 @eq-ch15-covariance-matrix 计算样本协方差
manual_eigenvalues = np.linalg.eigvalsh(sample_covariance)[::-1] # 利用对称矩阵求解并降序排列特征值
variance_table = pd.DataFrame({'手工特征值': manual_eigenvalues, 'sklearn 特征值': pca_model.explained_variance_, '解释方差比': pca_model.explained_variance_ratio_, '累计解释比': pca_model.explained_variance_ratio_.cumsum()}, index=[f'PC{component_number}' for component_number in range(1, len(numeric_features) + 1)]) # 对照手工推导与库实现
assert np.allclose(manual_eigenvalues, pca_model.explained_variance_) # 验证公式与 sklearn 结果数值一致
variance_table.round(4) # 输出选择成分数所需的方差证据
图 15.1 将前两个主成分与载荷放在同一图中。点表示公司—季度观测,箭头表示原始特征沿两个成分的方向。载荷必须与定义、分布和具体样本联合解读,不是自动命名的『价值因子』。
import matplotlib.pyplot as plt # 绘制由真实数据拟合得到的 PCA 结果
plt.rcParams['font.family'] = ['Source Han Serif SC'] # 按项目规范使用思源宋体
plt.rcParams['axes.unicode_minus'] = False # 保证中文字体下负号正确显示
display_positions = np.linspace(0, len(training_scores) - 1, min(1200, len(training_scores)), dtype=int) # 等距抽取点仅用于避免图形遮挡
figure, axis = plt.subplots(figsize=(8, 6)) # 创建可容纳散点和载荷的画布
axis.scatter(training_scores[display_positions, 0], training_scores[display_positions, 1], s=10, alpha=0.18, color='#2C3E50', label='训练期观测') # 展示历史样本的主成分得分
arrow_scale = 3.0 # 将单位载荷缩放到散点图可见范围
for feature_name, first_loading, second_loading in zip(numeric_features, pca_model.components_[0], pca_model.components_[1]): # 逐一标注原始特征的投影方向
axis.arrow(0, 0, first_loading * arrow_scale, second_loading * arrow_scale, color='#E3120B', width=0.012, head_width=0.10, length_includes_head=True) # 用箭头表示特征载荷
axis.text(first_loading * arrow_scale * 1.12, second_loading * arrow_scale * 1.12, feature_name, fontsize=8, color='#E3120B') # 将特征名紧邻对应箭头
axis.axhline(0, color='#8E9EAA', linewidth=0.8) # 添加第二主成分的零基准
axis.axvline(0, color='#8E9EAA', linewidth=0.8) # 添加第一主成分的零基准
axis.set_xlabel(f'PC1({pca_model.explained_variance_ratio_[0]:.1%})') # 在横轴同时披露解释方差比
axis.set_ylabel(f'PC2({pca_model.explained_variance_ratio_[1]:.1%})') # 在纵轴同时披露解释方差比
axis.set_title('长三角上市公司估值特征 PCA') # 说明图形对象和方法
axis.legend(frameon=False) # 以简洁图例标识样本点
plt.tight_layout() # 避免载荷文本与边界重叠
plt.show() # 将拟合结果交给 Quarto 渲染
适用边界
PCA 最大化的是无监督方差,不是样本外收益的可预测性。高方差可能来自市场泡沫、会计口径差异或错误值。在监督学习中,成分数必须放在交叉验证内选择;主成分也不能解释为因果因子。
不均衡不是数据清洗问题
截面前五分位正类约占 20%,因此全部预测为 0 就可以获得约 80% 准确率,却没有识别前五分位样本的能力。平衡类权重通常设为
\[
\omega_k=\frac{n}{K n_k},
\tag{15.9}\]
式 式 15.9 使每个类别在加权样本总量中贡献相同。其中 \(n_k\) 是类别 \(k\) 的样本数,\(K\) 是类别数。这个权重修改了误分成本,却不会创造新信息。对罕见正类,应同时报告精确率、召回率、PR-AUC 和根据交易成本确定的阈值;ROC-AUC 可以作为排序指标,但不能单独表示罕见事件的实用性。
常见误区
先变换、后拆分:即使没有用目标,测试均值、分位数和 PCA 方向也是未来信息。
全样本目标编码:平滑只降低方差;按标签可得日 purge 的时间 OOF 才能避免尚未形成的结果进入映射。
把缩尾当作去伪:负 PE 可能反映真实亏损,不能仅因其不符合常态分布就删除。
用 PCA 解释因果:载荷描述样本协方差结构,不识别经济因果。
只看准确率:类别不均衡时,高准确率可能对应零正类召回。
本章小结
数值缩放改变几何尺度,缩尾限制尾部影响,分箱用信息损失换取稳健与非线性。目标编码的关键不是 groupby().mean(),而是平滑、标签可得日 purge、时间 OOF 与样本外未见类别规则;GroupKFold 只能另行回答公司是否跨侧。PCA 由协方差矩阵的特征分解导出,必须与载荷、解释方差和稳定性诊断一起使用。所有拟合型变换和目标统计都只能在当时已知的信息集上估计;这一原则将在下一章通过 Pipeline 工程化。
分层练习
基础题
练习 15.1
某特征的训练值为 \([2,4,6,8]\)。按 StandardScaler 约定计算均值、方差和标准化结果;说明为何不应将测试值 100 加入参数估计。
完整解答
\(μ=(2+4+6+8)/4=5\),\(σ^2=[9+1+1+9]/4=5\),\(σ=√5\)。因此 Z 分数依次为 \([-3/√5,-1/√5,1/√5,3/√5]\),约为 \([-1.342,-0.447,0.447,1.342]\)。测试值 100 是拟真评估中的未来信息;把它加入均值和方差会改变训练样本的坐标系,使评估过于乐观。
进阶题
练习 15.2
对比手工 PCA 特征值与 sklearn 结果,选出累计解释方差首次达到 80% 所需的成分数,并找出 PC1 绝对载荷最大的特征。
完整解答
参考代码见 列表 15.4。
输出中成分数由当前本地数据决定。特征值差异应仅处于浮点误差量级;若差异很大,应先检查是否忘记中心化、是否使用相同的协方差分母。
真实数据应用题
练习 15.3
证明时间 OOF 每折拟合端的非法标签数为 0,并核验面向测试集的映射未使用测试信息时点尚不可得的标签。另用 GroupKFold 证明公司交集为 0,说明为何这一结果不能替代时间审计;最后比较时间 OOF 与错误全样本编码对罕见行业的影响。
完整解答
分组断言和对照计算见 列表 15.5。
时间 OOF 的非法标签数与最终训练集非法标签数必须全为零;GroupKFold 的公司交集也必须全为零,但后者只回答实体隔离,不能证明验证时点的标签已经可得。罕见行业中单个目标对类别均值影响更大,因此安全时间 OOF 与错误全样本编码的差异通常更明显。
综合挑战题
练习 15.4
对『下一季度市值变化率位于同期截面前五分位』任务计算平衡类权重。说明为何不应使用 SMOTE 后再拆分时间测试集,并给出正确顺序。
完整解答
类别频数与平衡权重的对照见 表 15.9。
from sklearn.utils.class_weight import compute_class_weight # 使用与 sklearn 分类器一致的权重定义
observed_classes = np.sort(training_panel['is_top_quintile'].unique()) # 明确训练期实际出现的类别
balanced_weights = compute_class_weight(class_weight='balanced', classes=observed_classes, y=training_panel['is_top_quintile']) # 实现 @eq-ch15-balanced-weight
class_weight_table = pd.DataFrame({'类别': observed_classes, '样本数': training_panel['is_top_quintile'].value_counts().reindex(observed_classes).to_numpy(), '平衡权重': balanced_weights}) # 对照频数与损失权重
class_weight_table.round(3) # 显示少数类获得更高权重的结果
正确顺序是:先按时间留出最终测试集;然后在每个训练折内拟合预处理和任何重采样器;最后对该折的未处理验证数据评分。如果在拆分前做 SMOTE,由测试样本参与构造的合成点可能进入训练集,从而直接泄漏未来信息。
变换边界比较题
练习 15.5
训练值为 \([0,10,20,1000]\),未来测试值为 1500。按训练期参数计算测试值的 Z 分数和 Min–Max 值;再用训练期 25% 与 75% 分位数缩尾测试值。比较三种变换对未来超界、异常值和排序信息的处理边界,并说明对数变换为何不能替代缩尾。
完整解答
训练均值为 257.5,总体标准差约为 428.741,所以测试值的 Z 分数约为 2.898;Min–Max 值为 \((1500-0)/(1000-0)=1.5\),说明测试值不会被自动限制在 \([0,1]\)。训练期 25% 与 75% 分位数分别为 7.5 和 265,因此缩尾后的测试值为 265。
表 表 15.10 汇总本节的计算或审计结果,解释时应遵循正文给出的口径与限制。
boundary_training_values = pd.Series([0.0, 10.0, 20.0, 1000.0]) # 构造题设中的历史拟合样本
boundary_test_value = 1500.0 # 将未来极端值严格排除在参数估计之外
training_mean = boundary_training_values.mean() # 按StandardScaler口径估计训练均值
training_scale = boundary_training_values.std(ddof=0) # 按总体方差约定估计训练尺度
test_z_score = (boundary_test_value - training_mean) / training_scale # 用历史尺度变换未来值而不截断
test_minmax = (boundary_test_value - boundary_training_values.min()) / (boundary_training_values.max() - boundary_training_values.min()) # 允许未来值超出训练区间
winsor_limits = boundary_training_values.quantile([0.25, 0.75]) # 仅用训练样本估计教学用缩尾边界
test_winsorized = np.clip(boundary_test_value, winsor_limits.loc[0.25], winsor_limits.loc[0.75]) # 将未来尾部压到历史上界
transform_boundary_table = pd.DataFrame({'变换': ['Z分数', 'Min–Max', '25%—75%缩尾'], '测试结果': [test_z_score, test_minmax, test_winsorized], '硬边界': ['无', '训练期为[0,1],未来可超界', '由训练分位数截断']}) # 并列展示三种变换的可观察差异
transform_boundary_table.round(3) # 输出边界比较证据
Z 分数和 Min–Max 都保持线性次序,却仍受训练极值影响;缩尾限制尾部影响,但会制造边界并列并改变距离。对数变换只能缓和正值右偏,对零值、负值需要另定规则,而且不会给极端值设置硬上界,所以不能当作缩尾的同义操作。