# 标准化性能优化:线性映射 vs 百分位排名 ## 问题背景 当前标准化系统使用 `series.rank(pct=True)` 方法,需要排序操作,时间复杂度 O(n log n)。 对于大规模数据(18,004个样本,未来可能更多),考虑使用**线性映射**替代方案,时间复杂度 O(n)。 --- ## 方案对比 ### **方案1: 当前百分位排名法 (Rank-based)** ```python def normalize_standard(series: pd.Series) -> pd.Series: """当前实现: 百分位排名""" return series.rank(pct=True) # O(n log n) ``` **特点:** - ✅ 输出分布均匀,严格单调 - ✅ 对极端值鲁棒,异常值不影响整体分布 - ✅ 保证中位数精确=0.5 - ❌ 时间复杂度 O(n log n)(排序) - ❌ 大数据集性能瓶颈 --- ### **方案2: 线性映射法 (Linear Scaling)** #### **2.1 基础版:P5-P95映射** ```python def normalize_linear_basic(series: pd.Series) -> pd.Series: """ 线性映射标准化:将P5-P95区间映射到[0, 1] 策略: 1. 计算P5和P95分位数 2. 线性缩放:y = (x - P5) / (P95 - P5) 3. Clip到[0, 1]区间 时间复杂度:O(n) """ p5 = series.quantile(0.05) # O(n) - numpy.percentile使用快速选择 p95 = series.quantile(0.95) # O(n) if p95 - p5 < 1e-10: # 避免除零,所有值相同时返回0.5 return pd.Series(0.5, index=series.index, dtype=float) # 线性缩放 normalized = (series - p5) / (p95 - p5) # Clip到[0, 1] normalized = normalized.clip(0, 1) return normalized ``` **特点:** - ✅ 时间复杂度 O(n)(快速选择算法) - ✅ 简单直观,易于理解 - ❌ 中位数不一定=0.5(除非原始数据对称分布) - ❌ 对极端值敏感(P5/P95位置影响整体) - ❌ 输出分布不均匀(保留原始分布形状) #### **2.2 改进版:中位数强制对齐** ```python def normalize_linear_median_aligned(series: pd.Series) -> pd.Series: """ 线性映射 + 中位数对齐到0.5 策略: 1. 计算中位数 M 2. 上半部分映射到[0.5, 1.0] 3. 下半部分映射到[0.0, 0.5] 时间复杂度:O(n) """ median = series.median() # O(n) # 计算上下分位数(用于映射范围) upper_bound = series.quantile(0.95) # O(n) lower_bound = series.quantile(0.05) # O(n) result = pd.Series(0.5, index=series.index, dtype=float) # 上半部分:[median, upper_bound] -> [0.5, 1.0] upper_mask = series >= median if upper_bound > median: upper_values = (series[upper_mask] - median) / (upper_bound - median) result[upper_mask] = 0.5 + 0.5 * upper_values.clip(0, 1) # 下半部分:[lower_bound, median] -> [0.0, 0.5] lower_mask = series < median if median > lower_bound: lower_values = (series[lower_mask] - lower_bound) / (median - lower_bound) result[lower_mask] = 0.5 * lower_values.clip(0, 1) return result ``` **特点:** - ✅ 时间复杂度 O(n) - ✅ 中位数严格=0.5 - ✅ 对上下半部分独立缩放,更灵活 - ❌ 上下分布可能不对称 - ❌ 仍对极端值敏感 #### **2.3 混合版:线性映射 + 尾部Clip** ```python def normalize_linear_hybrid( series: pd.Series, lower_pct: float = 0.05, upper_pct: float = 0.95 ) -> pd.Series: """ 混合方案:线性映射主体 + 百分位Clip尾部 策略: 1. 使用P5-P95线性映射主体(90%数据) 2. P95的映射到[0.9, 1.0] 3. 后处理:平移使中位数=0.5 时间复杂度:O(n) """ p_lower = series.quantile(lower_pct) # O(n) p_upper = series.quantile(upper_pct) # O(n) median = series.median() # O(n) if p_upper - p_lower < 1e-10: return pd.Series(0.5, index=series.index, dtype=float) # 线性缩放主体 normalized = (series - p_lower) / (p_upper - p_lower) # Clip到[0, 1] normalized = normalized.clip(0, 1) # 计算当前中位数 current_median = normalized.median() # 平移使中位数=0.5 shift = 0.5 - current_median normalized = (normalized + shift).clip(0, 1) return normalized ``` **特点:** - ✅ 时间复杂度 O(n) - ✅ 中位数接近0.5(通过平移调整) - ✅ 对极端值有一定鲁棒性 - ⚠️ 平移可能导致边界溢出,需要二次Clip --- ## 性能基准测试 ### 测试代码 ```python import pandas as pd import numpy as np import time # 生成测试数据 np.random.seed(42) sizes = [1_000, 10_000, 100_000, 1_000_000] for n in sizes: # 模拟不同分布 data_normal = pd.Series(np.random.randn(n)) data_skewed = pd.Series(np.random.exponential(1.0, n)) data_uniform = pd.Series(np.random.uniform(0, 1, n)) for name, data in [("Normal", data_normal), ("Skewed", data_skewed), ("Uniform", data_uniform)]: print(f"\n{name} Distribution, n={n:,}") # 方法1: Rank-based t0 = time.time() result1 = data.rank(pct=True) t1 = time.time() - t0 median1 = result1.median() # 方法2: Linear Basic t0 = time.time() p5, p95 = data.quantile(0.05), data.quantile(0.95) result2 = ((data - p5) / (p95 - p5)).clip(0, 1) t2 = time.time() - t0 median2 = result2.median() # 方法3: Linear Median-Aligned t0 = time.time() result3 = normalize_linear_median_aligned(data) t3 = time.time() - t0 median3 = result3.median() print(f" Rank-based: {t1*1000:6.2f}ms, median={median1:.4f}") print(f" Linear Basic: {t2*1000:6.2f}ms, median={median2:.4f}, speedup={t1/t2:.2f}x") print(f" Linear Aligned: {t3*1000:6.2f}ms, median={median3:.4f}, speedup={t1/t3:.2f}x") ``` ### 预期结果 | 数据量 | Rank-based | Linear Basic | Linear Aligned | 加速比 | |--------|-----------|--------------|----------------|--------| | 1K | 0.5ms | 0.2ms | 0.3ms | 2x | | 10K | 5ms | 1ms | 1.5ms | 4x | | 100K | 60ms | 8ms | 12ms | 6x | | 1M | 800ms | 80ms | 120ms | 8x | **结论**:数据量越大,线性映射的优势越明显。 --- ## 质量评估 ### 评估指标 1. **中位数偏差**:`|median - 0.5|`,越小越好 2. **分布均匀性**:Kolmogorov-Smirnov检验与均匀分布的距离 3. **单调性保持**:是否保持原始数据的排序关系 4. **极值鲁棒性**:添加10%极端异常值后的稳定性 ### 质量对比 ```python def evaluate_normalization(series: pd.Series, normalized: pd.Series): """评估标准化质量""" from scipy import stats # 1. 中位数偏差 median_error = abs(normalized.median() - 0.5) # 2. 分布均匀性(KS检验) ks_stat, ks_pvalue = stats.kstest(normalized.dropna(), 'uniform', args=(0, 1)) # 3. 单调性(Spearman相关系数应该=1) spearman_corr = stats.spearmanr(series, normalized).correlation # 4. 极值鲁棒性测试 series_with_outliers = series.copy() n_outliers = int(len(series) * 0.1) series_with_outliers.iloc[:n_outliers] = series.max() * 100 # 添加极端值 normalized_robust = normalize_function(series_with_outliers) median_change = abs(normalized_robust.median() - normalized.median()) return { 'median_error': median_error, 'ks_stat': ks_stat, 'uniformity': 1 - ks_stat, # 越接近1越均匀 'monotonicity': spearman_corr, 'robustness': 1 - median_change, # 越接近1越鲁棒 } ``` ### 预期质量对比 | 方法 | 中位数偏差 | 均匀性 | 单调性 | 鲁棒性 | 综合评分 | |------|-----------|--------|--------|--------|----------| | Rank-based | 0.0000 | **0.98** | 1.00 | **0.95** | **0.98** | | Linear Basic | 0.05-0.15 | 0.75 | 1.00 | 0.60 | 0.75 | | Linear Aligned | **0.0000** | 0.80 | 1.00 | 0.70 | 0.83 | | Linear Hybrid | 0.01-0.03 | 0.85 | 1.00 | 0.80 | 0.88 | --- ## 推荐方案 ### 方案A: 保持现状(推荐用于生产环境) **适用场景**:对质量要求高,性能可接受 ```python # 不修改,继续使用 rank(pct=True) def normalize_standard(series: pd.Series) -> pd.Series: return series.rank(pct=True) ``` **理由**: - 18,004个样本量下,性能差异可忽略(<100ms) - 质量最优,中位数严格=0.5,分布最均匀 - 已验证稳定,不引入新风险 --- ### 方案B: 混合策略(推荐用于实验) **适用场景**:需要性能提升,可接受轻微质量损失 ```python def normalize_standard_fast(series: pd.Series, threshold: int = 50000) -> pd.Series: """ 智能选择标准化方法 - n < threshold: 使用Rank-based(质量优先) - n >= threshold: 使用Linear Hybrid(性能优先) """ if len(series) < threshold: return series.rank(pct=True) else: return normalize_linear_hybrid(series) ``` **理由**: - 小数据集(<5万):用Rank-based,性能差异可忽略 - 大数据集(≥5万):用Linear Hybrid,性能提升明显 - 自适应平衡质量和性能 --- ### 方案C: 全面切换(仅当性能成为瓶颈时) **适用场景**:百万级样本,性能是硬约束 ```python # 全面替换为 Linear Median-Aligned def normalize_standard(series: pd.Series) -> pd.Series: return normalize_linear_median_aligned(series) ``` **理由**: - 8倍性能提升 - 中位数严格=0.5 - 质量评分0.83(vs 0.98)可接受 **代价**: - 分布均匀性下降 - 对极端值敏感性增加 - 需要全面回归测试 --- ## 实施建议 ### 短期(1周内) 1. **性能基准测试**:运行上述测试代码,获取实际数据 2. **质量评估**:对18,004样本数据集进行质量对比 3. **决策**:根据实测结果决定是否优化 ### 中期(1-2月) 如果性能确实是瓶颈: 1. **实现方案B**(混合策略) 2. **A/B测试**:对比两种方法的信号质量 3. **监控指标**:跟踪标准化后的中位数/分布变化 ### 长期(3-6月) 如果数据量持续增长(百万级): 1. **考虑方案C**(全面切换) 2. **或引入增量标准化**:预计算分位数,增量更新 3. **或引入采样标准化**:大数据集用采样估计分位数 --- ## 实验脚本 我可以创建一个完整的对比脚本,帮你评估: ```bash # 在 technical-patterns-lab 项目中 python scripts/scoring/benchmark_normalization.py ``` 输出: - 性能对比表格 - 质量评估报告 - 分布对比图表 - 推荐决策 --- ## 结论 **对于当前18,004样本的数据集,建议保持现状(方案A)**,理由: 1. ✅ **性能可接受**:rank(pct=True) 在1-2万样本下<100ms,不是瓶颈 2. ✅ **质量最优**:中位数严格=0.5,分布最均匀,对极端值鲁棒 3. ✅ **已验证稳定**:基于此方法的预设模式已优化,不引入新风险 **仅当满足以下条件之一时考虑线性映射优化**: - 样本量 > 10万 - 标准化耗时 > 500ms - 需要实时计算(在线标准化场景) **如果未来需要优化,推荐方案B(混合策略)**: - 平衡质量和性能 - 向下兼容,风险可控 - 可根据实际数据量自适应选择