technical-patterns-lab/discuss/20260130-标准化性能优化_线性映射方案.md
褚宏光 bf6baa5483 Add scoring module and enhance HTML viewer with standardization
- Add scripts/scoring/ module with normalizer, sensitivity analysis, and config
- Enhance stock_viewer.html with standardized scoring display
- Add integration tests and normalization verification scripts
- Add documentation for standardization implementation and usage guides
- Add data distribution analysis reports for strength scoring dimensions
- Update discussion documents with algorithm optimization plans
2026-01-30 18:43:37 +08:00

399 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 标准化性能优化:线性映射 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. <P5的映射到[0, 0.1]>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.83vs 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混合策略**
- 平衡质量和性能
- 向下兼容,风险可控
- 可根据实际数据量自适应选择