- 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
11 KiB
11 KiB
标准化性能优化:线性映射 vs 百分位排名
问题背景
当前标准化系统使用 series.rank(pct=True) 方法,需要排序操作,时间复杂度 O(n log n)。
对于大规模数据(18,004个样本,未来可能更多),考虑使用线性映射替代方案,时间复杂度 O(n)。
方案对比
方案1: 当前百分位排名法 (Rank-based)
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映射
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 改进版:中位数强制对齐
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
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
性能基准测试
测试代码
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 |
结论:数据量越大,线性映射的优势越明显。
质量评估
评估指标
- 中位数偏差:
|median - 0.5|,越小越好 - 分布均匀性:Kolmogorov-Smirnov检验与均匀分布的距离
- 单调性保持:是否保持原始数据的排序关系
- 极值鲁棒性:添加10%极端异常值后的稳定性
质量对比
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: 保持现状(推荐用于生产环境)
适用场景:对质量要求高,性能可接受
# 不修改,继续使用 rank(pct=True)
def normalize_standard(series: pd.Series) -> pd.Series:
return series.rank(pct=True)
理由:
- 18,004个样本量下,性能差异可忽略(<100ms)
- 质量最优,中位数严格=0.5,分布最均匀
- 已验证稳定,不引入新风险
方案B: 混合策略(推荐用于实验)
适用场景:需要性能提升,可接受轻微质量损失
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: 全面切换(仅当性能成为瓶颈时)
适用场景:百万级样本,性能是硬约束
# 全面替换为 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周内)
- 性能基准测试:运行上述测试代码,获取实际数据
- 质量评估:对18,004样本数据集进行质量对比
- 决策:根据实测结果决定是否优化
中期(1-2月)
如果性能确实是瓶颈:
- 实现方案B(混合策略)
- A/B测试:对比两种方法的信号质量
- 监控指标:跟踪标准化后的中位数/分布变化
长期(3-6月)
如果数据量持续增长(百万级):
- 考虑方案C(全面切换)
- 或引入增量标准化:预计算分位数,增量更新
- 或引入采样标准化:大数据集用采样估计分位数
实验脚本
我可以创建一个完整的对比脚本,帮你评估:
# 在 technical-patterns-lab 项目中
python scripts/scoring/benchmark_normalization.py
输出:
- 性能对比表格
- 质量评估报告
- 分布对比图表
- 推荐决策
结论
对于当前18,004样本的数据集,建议保持现状(方案A),理由:
- ✅ 性能可接受:rank(pct=True) 在1-2万样本下<100ms,不是瓶颈
- ✅ 质量最优:中位数严格=0.5,分布最均匀,对极端值鲁棒
- ✅ 已验证稳定:基于此方法的预设模式已优化,不引入新风险
仅当满足以下条件之一时考虑线性映射优化:
- 样本量 > 10万
- 标准化耗时 > 500ms
- 需要实时计算(在线标准化场景)
如果未来需要优化,推荐方案B(混合策略):
- 平衡质量和性能
- 向下兼容,风险可控
- 可根据实际数据量自适应选择