--- phase: 11-predictv3 plan: 01 type: execute wave: 1 depends_on: [] files_modified: - application/admin/model/History.php autonomous: true requirements: - PRED-02 - PRED-05 must_haves: truths: - "用户可以在回测结果中看到 NDCG@5 指标" - "用户可以在回测结果中看到 MRR 指标" - "用户可以看到各排名位置的命中分布统计" - "系统在数据不足时返回合理的默认值或提示" artifacts: - path: "application/admin/model/History.php" provides: "NDCG、MRR、命中分布计算方法" contains: "_calculateNDCG|_calculateMRR|_calculateHitDistribution" key_links: - from: "_runBacktestV3" to: "_calculateNDCG, _calculateMRR, _calculateHitDistribution" via: "method call in return statement" --- # Phase 11 - Plan 01: 回测指标扩展 ## Objective 扩展 `_runBacktestV3` 方法的回测指标,新增 NDCG@5、MRR、命中率分布等排名质量评估指标,提升算法评估能力。 **Purpose:** 当前回测仅返回命中率(Top5)和平均排名,缺少排名质量评估指标。NDCG、MRR 是成熟的推荐系统评估指标,能更全面反映预测排名质量。 **Output:** `History.php` 中新增 3 个计算方法,`_runBacktestV3` 返回结果扩展。 ## Tasks ### Task 1: 实现 NDCG@5 计算(含空预测保护和公式文档) - D:\code\php\amlhc\application\admin\model\History.php (line 3495-3560, _runBacktestV3 方法) 在 `History.php` 文件末尾(类内)新增 `_calculateNDCG` 方法: ```php /** * 计算 NDCG@K (Normalized Discounted Cumulative Gain) * * 公式说明: * - DCG (Discounted Cumulative Gain) = Σ(rel_i / log2(rank_i + 1)) * 其中 rel_i = 1 (命中) 或 0 (未命中),rank_i 为预测排名位置 * - IDCG (Ideal DCG) = Σ(1 / log2(i + 1)) for i = 1..min(hits, K) * 即理想情况下所有命中的号码都排在最前面的DCG值 * - NDCG = DCG / IDCG,范围 0-1,越接近1表示排名质量越好 * * @param array $backtestDetails 回测详情数组,每项包含 {hit: bool, rank: int} * @param int $K Top-K 参数,默认5,评估前K个预测位置的排名质量 * @return float NDCG值 (0-1范围),空数据时返回0 */ private function _calculateNDCG($backtestDetails, $K = 5) { // 边缘情况处理:空预测或无效参数 if (empty($backtestDetails) || $K <= 0) { return 0; } $dcg = 0; $idcg = 0; // 计算 DCG: 命中号码的排名折损累积值 foreach ($backtestDetails as $detail) { if (!isset($detail['hit']) || !isset($detail['rank'])) { continue; // 跳过无效数据 } if ($detail['hit'] && $detail['rank'] > 0 && $detail['rank'] <= $K) { // DCG公式: rel / log2(rank + 1),命中时 rel=1 $dcg += 1 / log($detail['rank'] + 1, 2); } } // 计算 IDCG: 最理想情况下所有命中的 DCG(假设都排在第1位) $hitCount = 0; foreach ($backtestDetails as $detail) { if (isset($detail['hit']) && $detail['hit']) { $hitCount++; } } for ($i = 1; $i <= min($hitCount, $K); $i++) { $idcg += 1 / log($i + 1, 2); } // 返回标准化值,IDCG为0时返回0避免除零错误 return $idcg > 0 ? round($dcg / $idcg, 4) : 0; } ``` 实现要点: - 公式:DCG = Σ(1/log2(rank+1)),IDCG = Σ(1/log2(i+1)) for i=1..hits - 添加空预测保护:检查 $backtestDetails 是否为空 - 添加数据完整性检查:确保 hit 和 rank 字段存在 - 使用 log(rank + 1, 2) 作为折损函数,排名越靠前权重越高 - 返回 0-1 范围的标准化值,越接近 1 表示排名质量越好 - grep 正则匹配: `_calculateNDCG\s*\(` 在 History.php 中存在 - grep 匹配: `empty($backtestDetails)` 在方法中存在(空预测保护) - 方法返回 float 类型值 - 包含函数级注释说明 NDCG 计算逻辑和公式 ### Task 2: 实现 MRR 和命中分布计算(含边缘情况处理) - D:\code\php\amlhc\application\admin\model\History.php (新增的 _calculateNDCG 方法位置) 在 `_calculateNDCG` 方法后继续新增 `_calculateMRR` 和 `_calculateHitDistribution` 方法: ```php /** * 计算 MRR (Mean Reciprocal Rank) * 平均倒数排名,关注命中号码的具体排名位置 * * 公式说明: * - MRR = Σ(1/rank_i) / N,其中 rank_i 为命中号码的排名,N 为测试总数 * - 未命中的测试项贡献 0 到倒数排名 * - MRR 范围 0-1,越接近1表示命中号码平均排名越靠前 * * @param array $backtestDetails 回测详情数组,每项包含 {hit: bool, rank: int} * @return float MRR值 (0-1范围),空数据时返回0 */ private function _calculateMRR($backtestDetails) { // 边缘情况处理:空预测 if (empty($backtestDetails)) { return 0; } $reciprocalRanks = []; foreach ($backtestDetails as $detail) { if (!isset($detail['hit']) || !isset($detail['rank'])) { continue; // 跳过无效数据 } if ($detail['hit'] && $detail['rank'] > 0) { $reciprocalRanks[] = 1 / $detail['rank']; } else { $reciprocalRanks[] = 0; // 未命中记为0 } } return count($reciprocalRanks) > 0 ? round(array_sum($reciprocalRanks) / count($reciprocalRanks), 4) : 0; } /** * 计算命中率分布 * 统计各排名位置(1-5)的命中次数分布 * * 结构定义: * - 返回格式: {rank_1: n, rank_2: n, rank_3: n, rank_4: n, rank_5: n} * - rank_N 表示预测排名第N位的命中次数 * - 用于前端柱状图可视化展示 * * @param array $backtestDetails 回测详情数组,每项包含 {hit: bool, rank: int} * @return array 各排名(1-5)的命中次数统计,键名为 rank_1 到 rank_5 */ private function _calculateHitDistribution($backtestDetails) { // 边缘情况处理:空预测返回全0分布 if (empty($backtestDetails)) { return [ 'rank_1' => 0, 'rank_2' => 0, 'rank_3' => 0, 'rank_4' => 0, 'rank_5' => 0 ]; } // 初始化分布数组,键名使用 rank_N 格式便于前端解析 $distribution = [ 'rank_1' => 0, 'rank_2' => 0, 'rank_3' => 0, 'rank_4' => 0, 'rank_5' => 0 ]; foreach ($backtestDetails as $detail) { if (!isset($detail['hit']) || !isset($detail['rank'])) { continue; // 跳过无效数据 } if ($detail['hit'] && $detail['rank'] >= 1 && $detail['rank'] <= 5) { $key = 'rank_' . $detail['rank']; $distribution[$key]++; } } return $distribution; } ``` 实现要点: - MRR: 命中号码排名倒数平均值,公式 Σ(1/rank)/N - 命中分布: 明确结构为 `{rank_1: n, rank_2: n, ..., rank_5: n}` - 两个方法均添加空预测保护和无效数据跳过逻辑 - hit_distribution 使用 rank_N 键名格式,便于前端柱状图渲染 - grep 正则匹配: `_calculateMRR\s*\(` 在 History.php 中存在 - grep 正则匹配: `_calculateHitDistribution\s*\(` 在 History.php 中存在 - grep 匹配: `empty($backtestDetails)` 在两个方法中均存在(空预测保护) - grep 匹配: `rank_1|rank_2|rank_3|rank_4|rank_5` 在 _calculateHitDistribution 中存在 - 两个方法均包含函数级注释 ### Task 3: 扩展 _runBacktestV3 返回结果(含数据量检查) - D:\code\php\amlhc\application\admin\model\History.php (line 3549-3556, _runBacktestV3 返回语句) 修改 `_runBacktestV3` 方法的返回语句,在原有返回结构中添加新指标和数据量验证: 找到以下代码段(约 line 3549-3556): ```php return [ 'hit_rate' => $hitRate, 'avg_rank' => $avgRank, 'total_tests' => $testCount, 'total_hits' => $hits, 'details' => $details ]; ``` 替换为: ```php // 计算新增指标(添加数据量检查) $minDataThreshold = 50; // 置信度计算最小数据量阈值 // 如果测试数据不足,返回默认值并添加警告 if ($testCount < $minDataThreshold) { $ndcg5 = 0; $mrr = 0; $hitDistribution = [ 'rank_1' => 0, 'rank_2' => 0, 'rank_3' => 0, 'rank_4' => 0, 'rank_5' => 0 ]; $dataWarning = '回测数据不足(' . $testCount . '期),建议至少50期以获得可靠指标'; } else { $ndcg5 = $this->_calculateNDCG($details, 5); $mrr = $this->_calculateMRR($details); $hitDistribution = $this->_calculateHitDistribution($details); $dataWarning = null; } $precision5 = $testCount > 0 ? round($hits / ($testCount * 5) * 100, 2) : 0; return [ 'hit_rate' => $hitRate, 'avg_rank' => $avgRank, 'total_tests' => $testCount, 'total_hits' => $hits, 'details' => $details, // 新增排名质量指标 'ndcg_5' => $ndcg5, 'mrr' => $mrr, 'hit_distribution' => $hitDistribution, 'precision_5' => $precision5, // 数据量警告(不足时提示) 'data_warning' => $dataWarning, 'data_sufficient' => $testCount >= $minDataThreshold ]; ``` 注意: - 新增指标计算放在 return 语句之前,确保 $details 数组已完整构建 - 添加最小数据量检查(50期),不足时返回默认值和警告提示 - 新增 data_warning 和 data_sufficient 字段供前端展示 - grep 匹配: `ndcg_5` 在 _runBacktestV3 返回结构中存在 - grep 匹配: `mrr` 在 _runBacktestV3 返回结构中存在 - grep 匹配: `hit_distribution` 在 _runBacktestV3 返回结构中存在 - grep 匹配: `precision_5` 在 _runBacktestV3 返回结构中存在 - grep 匹配: `data_warning` 在 _runBacktestV3 返回结构中存在 - grep 匹配: `minDataThreshold` 变量在方法中存在 ## Verification 执行预测接口验证新指标返回: ```bash curl -s "http://127.0.0.1:8000/admin/history/predictV3?periods=200&backtest=10" | grep -E "ndcg_5|mrr|hit_distribution|precision_5|data_warning" ``` 预期结果:返回 JSON 中包含 ndcg_5、mrr、hit_distribution、precision_5、data_warning 字段。 ## Success Criteria 1. `_calculateNDCG`、`_calculateMRR`、`_calculateHitDistribution` 三个方法已实现 2. 所有计算方法包含空预测保护和数据完整性检查 3. NDCG 公式在注释中完整说明:DCG = Σ(1/log2(rank+1)) 4. hit_distribution 结构明确为 `{rank_1..rank_5: counts}` 格式 5. `_runBacktestV3` 返回结构包含 ndcg_5、mrr、hit_distribution、precision_5、data_warning 字段 6. 添加数据量检查,不足50期时返回警告 7. 所有新增方法包含函数级注释 ## Output 完成后创建 `.planning/phases/11-predictv3/11-01-SUMMARY.md`