- 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
399 lines
11 KiB
Markdown
399 lines
11 KiB
Markdown
# 标准化性能优化:线性映射 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.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(混合策略)**:
|
||
- 平衡质量和性能
|
||
- 向下兼容,风险可控
|
||
- 可根据实际数据量自适应选择
|