diff --git a/application/admin/model/History.php b/application/admin/model/History.php index 9925ae4..4c32c8b 100644 --- a/application/admin/model/History.php +++ b/application/admin/model/History.php @@ -1120,5 +1120,2607 @@ class History extends Model ]; } + /** + * 综合预测号码 + * 基于历史多维度转移概率分析,给出号码预测建议 + * @param int $periods 查询最近多少期用于转移概率统计 + * @param array $weights 各维度权重配置 + * @param string $targetExpect 目标期号(可选,用于验证历史预测成功率) + * @return array {predictions: [], last_special: int, analysis: {}, actual_result: {}, hit_info: {}} + */ + public function getPrediction($periods = 100, $weights = [], $targetExpect = '') + { + // 默认权重配置 + $defaultWeights = [ + 'zone' => 0.25, // 区域转移 + 'zodiac' => 0.20, // 生肖转移 + 'tail' => 0.20, // 尾号转移 + 'head' => 0.15, // 首号转移 + 'color' => 0.10, // 波色转移 + 'hotcold' => 0.10 // 冷热系数 + ]; + $weights = array_merge($defaultWeights, $weights); + + // 获取号码属性映射 + $num_model = new Num(); + $colorMap = $num_model->column('color', 'num'); + $animalMap = $num_model->column('animal', 'num'); + + // 辅助函数:获取号码所属维度索引 + $getZoneIdx = function ($num) { + if ($num <= 10) return 0; + if ($num <= 20) return 1; + if ($num <= 30) return 2; + if ($num <= 40) return 3; + return 4; + }; + + $getTailIdx = function ($num) { + return $num % 10; + }; + + $getHeadIdx = function ($num) { + return intval(floor($num / 10)); + }; + + $getZodiacIdx = function ($num) use ($animalMap) { + $animalLabels = ['鼠', '牛', '虎', '兔', '龙', '蛇', '马', '羊', '猴', '鸡', '狗', '猪']; + $animal = $animalMap[$num] ?? ''; + $idx = array_search($animal, $animalLabels); + return $idx === false ? -1 : $idx; + }; + + $getColorIdx = function ($num) use ($colorMap) { + $color = $colorMap[$num] ?? ''; + if (strpos($color, '红') !== false) return 0; + if (strpos($color, '蓝') !== false) return 1; + if (strpos($color, '绿') !== false) return 2; + return -1; + }; + + // 确定预测基准期和目标期 + $actualResult = null; + $lastSpecial = 0; + $lastExpect = ''; + $cutoffTime = null; + + if ($targetExpect) { + // 查找目标期号 + $targetRow = $this->where('expect', $targetExpect)->find(); + if (!$targetRow) { + return ['predictions' => [], 'error' => '期号不存在', 'target_expect' => $targetExpect]; + } + $cutoffTime = $targetRow['openTime']; + $actualResult = [ + 'expect' => (string)$targetRow['expect'], + 'num7' => (int)$targetRow['num7'], + 'color' => $colorMap[$targetRow['num7']] ?? '', + 'animal' => $animalMap[$targetRow['num7']] ?? '', + 'openTime' => $targetRow['openTime'] + ]; + + // 获取目标期号前一期的特码作为预测基准 + $prevRow = $this->where('openTime', '<', $cutoffTime)->order('openTime', 'desc')->limit(1)->find(); + if (!$prevRow) { + return ['predictions' => [], 'error' => '目标期号之前没有历史数据', 'target_expect' => $targetExpect]; + } + $lastSpecial = (int)$prevRow['num7']; + $lastExpect = (string)$prevRow['expect']; + } else { + // 使用最新一期作为预测基准 + $latest = $this->field('expect,num7,openTime')->order('openTime', 'desc')->limit(1)->find(); + if (!$latest) { + return ['predictions' => [], 'last_special' => 0, 'analysis' => []]; + } + $lastSpecial = (int)$latest['num7']; + $lastExpect = (string)$latest['expect']; + } + + // 计算指定时间点之前的转移概率 + $zoneData = $this->_calcTransitionAtTime('zone', $periods, $cutoffTime); + $zodiacData = $this->_calcTransitionAtTime('zodiac', $periods, $cutoffTime); + $tailData = $this->_calcTransitionAtTime('tail', $periods, $cutoffTime); + $headData = $this->_calcTransitionAtTime('head', $periods, $cutoffTime); + $colorData = $this->_calcTransitionAtTime('color', $periods, $cutoffTime); + + // 计算指定时间点的冷热状态 + $hotcoldStatus = $this->_calcHotcoldAtTime($lastSpecial, 30, $cutoffTime); + + // 获取上一期特码所在各维度 + $lastZone = $getZoneIdx($lastSpecial); + $lastTail = $getTailIdx($lastSpecial); + $lastHead = $getHeadIdx($lastSpecial); + $lastZodiac = $getZodiacIdx($lastSpecial); + $lastColor = $getColorIdx($lastSpecial); + + // 计算每个号码的综合预测得分 + $scores = []; + $analysis = [ + 'last_special' => $lastSpecial, + 'last_expect' => $lastExpect, + 'last_zone' => $zoneData['zones'][$lastZone] ?? '', + 'last_zodiac' => $zodiacData['animals'][$lastZodiac] ?? '', + 'last_tail' => $tailData['tails'][$lastTail] ?? '', + 'last_head' => $headData['heads'][$lastHead] ?? '', + 'last_color' => $colorData['colors'][$lastColor] ?? '', + 'weights' => $weights, + 'target_expect' => $targetExpect + ]; + + for ($num = 1; $num <= 49; $num++) { + $totalScore = 0; + $detail = []; + + // 区域转移得分 + $targetZone = $getZoneIdx($num); + if ($lastZone >= 0 && $lastZone < 5 && isset($zoneData['probabilities'][$lastZone][$targetZone])) { + $zoneProb = $zoneData['probabilities'][$lastZone][$targetZone]; + $zoneScore = $zoneProb * $weights['zone']; + $totalScore += $zoneScore; + $detail['zone_prob'] = $zoneProb; + } + + // 生肖转移得分 + $targetZodiac = $getZodiacIdx($num); + if ($lastZodiac >= 0 && $lastZodiac < 12 && $targetZodiac >= 0 && isset($zodiacData['probabilities'][$lastZodiac][$targetZodiac])) { + $zodiacProb = $zodiacData['probabilities'][$lastZodiac][$targetZodiac]; + $zodiacScore = $zodiacProb * $weights['zodiac']; + $totalScore += $zodiacScore; + $detail['zodiac_prob'] = $zodiacProb; + } + + // 尾号转移得分 + $targetTail = $getTailIdx($num); + if ($lastTail >= 0 && $lastTail < 10 && isset($tailData['probabilities'][$lastTail][$targetTail])) { + $tailProb = $tailData['probabilities'][$lastTail][$targetTail]; + $tailScore = $tailProb * $weights['tail']; + $totalScore += $tailScore; + $detail['tail_prob'] = $tailProb; + } + + // 首号转移得分 + $targetHead = $getHeadIdx($num); + if ($lastHead >= 0 && $lastHead < 5 && isset($headData['probabilities'][$lastHead][$targetHead])) { + $headProb = $headData['probabilities'][$lastHead][$targetHead]; + $headScore = $headProb * $weights['head']; + $totalScore += $headScore; + $detail['head_prob'] = $headProb; + } + + // 波色转移得分 + $targetColor = $getColorIdx($num); + if ($lastColor >= 0 && $lastColor < 3 && $targetColor >= 0 && isset($colorData['probabilities'][$lastColor][$targetColor])) { + $colorProb = $colorData['probabilities'][$lastColor][$targetColor]; + $colorScore = $colorProb * $weights['color']; + $totalScore += $colorScore; + $detail['color_prob'] = $colorProb; + } + + // 冷热系数得分 + $status = $hotcoldStatus[$num] ?? 'warm'; + $hotcoldFactor = 0; + if ($status === 'cold') { + $hotcoldFactor = 100; + } elseif ($status === 'warm') { + $hotcoldFactor = 50; + } else { + $hotcoldFactor = 20; + } + $hotcoldScore = $hotcoldFactor * $weights['hotcold']; + $totalScore += $hotcoldScore; + $detail['hotcold_status'] = $status; + $detail['hotcold_factor'] = $hotcoldFactor; + + $scores[$num] = [ + 'num' => $num, + 'score' => round($totalScore, 2), + 'color' => $colorMap[$num] ?? '', + 'animal' => $animalMap[$num] ?? '', + 'detail' => $detail + ]; + } + + // 按得分降序排序 + $predictions = array_values($scores); + usort($predictions, function ($a, $b) { + return $b['score'] - $a['score']; + }); + + // 只返回得分最高的前5个号码 + $predictions = array_slice($predictions, 0, 5); + + // 计算命中情况 + $hitInfo = null; + if ($actualResult) { + $hitRank = -1; + foreach ($predictions as $idx => $p) { + if ($p['num'] === $actualResult['num7']) { + $hitRank = $idx + 1; + break; + } + } + $hitInfo = [ + 'hit' => $hitRank > 0, + 'rank' => $hitRank, + 'actual_num' => $actualResult['num7'], + 'actual_color' => $actualResult['color'], + 'actual_animal' => $actualResult['animal'], + 'actual_expect' => $actualResult['expect'] + ]; + } + + return [ + 'predictions' => $predictions, + 'last_special' => $lastSpecial, + 'last_expect' => $lastExpect, + 'analysis' => $analysis, + 'actual_result' => $actualResult, + 'hit_info' => $hitInfo + ]; + } + + /** + * 计算指定时间点的转移概率 + * @param string $type 转移类型: zone/zodiac/tail/head/color + * @param int $periods 统计期数 + * @param string|null $cutoffTime 截断时间(该时间之后的数据不参与统计) + * @return array + */ + private function _calcTransitionAtTime($type, $periods, $cutoffTime = null) + { + $query = $this->field('expect,num7,openTime')->order('openTime', 'desc'); + if ($cutoffTime) { + $query = $query->where('openTime', '<', $cutoffTime); + } + $history = $query->limit($periods)->select(); + + if (empty($history) || count($history) < 2) { + return $this->_getEmptyTransition($type); + } + + $history = array_reverse($history); + + $num_model = new Num(); + $colorMap = $num_model->column('color', 'num'); + $animalMap = $num_model->column('animal', 'num'); + + $getZoneIdx = function ($num) { + if ($num <= 10) return 0; + if ($num <= 20) return 1; + if ($num <= 30) return 2; + if ($num <= 40) return 3; + return 4; + }; + + $getTailIdx = function ($num) { + return $num % 10; + }; + + $getHeadIdx = function ($num) { + return intval(floor($num / 10)); + }; + + $getZodiacIdx = function ($num) use ($animalMap) { + $animalLabels = ['鼠', '牛', '虎', '兔', '龙', '蛇', '马', '羊', '猴', '鸡', '狗', '猪']; + $animal = $animalMap[$num] ?? ''; + $idx = array_search($animal, $animalLabels); + return $idx === false ? -1 : $idx; + }; + + $getColorIdx = function ($num) use ($colorMap) { + $color = $colorMap[$num] ?? ''; + if (strpos($color, '红') !== false) return 0; + if (strpos($color, '蓝') !== false) return 1; + if (strpos($color, '绿') !== false) return 2; + return -1; + }; + + switch ($type) { + case 'zone': + $labels = ['1-10', '11-20', '21-30', '31-40', '41-49']; + $size = 5; + $getIdx = $getZoneIdx; + break; + case 'zodiac': + $labels = ['鼠', '牛', '虎', '兔', '龙', '蛇', '马', '羊', '猴', '鸡', '狗', '猪']; + $size = 12; + $getIdx = $getZodiacIdx; + break; + case 'tail': + $labels = ['尾0', '尾1', '尾2', '尾3', '尾4', '尾5', '尾6', '尾7', '尾8', '尾9']; + $size = 10; + $getIdx = $getTailIdx; + break; + case 'head': + $labels = ['首号0', '首号1', '首号2', '首号3', '首号4']; + $size = 5; + $getIdx = $getHeadIdx; + break; + case 'color': + $labels = ['红波', '蓝波', '绿波']; + $size = 3; + $getIdx = $getColorIdx; + break; + default: + return []; + } + + $keyField = $type === 'zone' ? 'zones' : ($type === 'zodiac' ? 'animals' : ($type === 'tail' ? 'tails' : ($type === 'head' ? 'heads' : 'colors'))); + $matrix = array_fill(0, $size, array_fill(0, $size, 0)); + $rowTotals = array_fill(0, $size, 0); + $totalTransitions = 0; + + for ($i = 0; $i < count($history) - 1; $i++) { + $currentNum = (int)$history[$i]['num7']; + $nextNum = (int)$history[$i + 1]['num7']; + $from = $getIdx($currentNum); + $to = $getIdx($nextNum); + if ($from < 0 || $to < 0) continue; + $matrix[$from][$to]++; + $rowTotals[$from]++; + $totalTransitions++; + } + + $probabilities = array_fill(0, $size, array_fill(0, $size, 0)); + for ($i = 0; $i < $size; $i++) { + if ($rowTotals[$i] > 0) { + for ($j = 0; $j < $size; $j++) { + $probabilities[$i][$j] = round($matrix[$i][$j] / $rowTotals[$i] * 100, 1); + } + } + } + + return [ + $keyField => $labels, + 'matrix' => $matrix, + 'probabilities' => $probabilities, + 'row_totals' => $rowTotals, + 'total_transitions' => $totalTransitions + ]; + } + + /** + * 获取空的转移概率结构 + */ + private function _getEmptyTransition($type) + { + switch ($type) { + case 'zone': + return ['zones' => ['1-10','11-20','21-30','31-40','41-49'], 'matrix' => [], 'probabilities' => [], 'row_totals' => [], 'total_transitions' => 0]; + case 'zodiac': + return ['animals' => ['鼠','牛','虎','兔','龙','蛇','马','羊','猴','鸡','狗','猪'], 'matrix' => [], 'probabilities' => [], 'row_totals' => [], 'total_transitions' => 0]; + case 'tail': + return ['tails' => ['尾0','尾1','尾2','尾3','尾4','尾5','尾6','尾7','尾8','尾9'], 'matrix' => [], 'probabilities' => [], 'row_totals' => [], 'total_transitions' => 0]; + case 'head': + return ['heads' => ['首号0','首号1','首号2','首号3','首号4'], 'matrix' => [], 'probabilities' => [], 'row_totals' => [], 'total_transitions' => 0]; + case 'color': + return ['colors' => ['红波','蓝波','绿波'], 'matrix' => [], 'probabilities' => [], 'row_totals' => [], 'total_transitions' => 0]; + default: + return []; + } + } + + /** + * 计算指定时间点的冷热状态 + * @param int $lastSpecial 前一期特码(用于确定统计截止点) + * @param int $lookback 回看期数 + * @param string|null $cutoffTime 截断时间 + * @return array 号码冷热状态映射 + */ + private function _calcHotcoldAtTime($lastSpecial, $lookback, $cutoffTime = null) + { + // 找到前一期的时间点 + if ($cutoffTime) { + $prevTime = $cutoffTime; + } else { + $prevRow = $this->field('openTime')->order('openTime', 'desc')->limit(1)->find(); + $prevTime = $prevRow ? $prevRow['openTime'] : null; + } + + if (!$prevTime) { + return []; + } + + // 统计该时间点之前 lookback 期的特码出现次数 + $history = $this->field('num7') + ->where('openTime', '<', $prevTime) + ->order('openTime', 'desc') + ->limit($lookback) + ->select(); + + $count = array_fill(1, 49, 0); + $periodCount = 0; + foreach ($history as $row) { + $num = (int)$row['num7']; + if ($num >= 1 && $num <= 49) { + $count[$num]++; + } + $periodCount++; + } + + $avgCount = $periodCount > 0 ? $periodCount / 49 : 0; + $status = []; + + for ($num = 1; $num <= 49; $num++) { + if ($avgCount > 0) { + if ($count[$num] > $avgCount * 1.5) { + $status[$num] = 'hot'; + } elseif ($count[$num] < $avgCount * 0.5) { + $status[$num] = 'cold'; + } else { + $status[$num] = 'warm'; + } + } else { + $status[$num] = 'warm'; + } + } + + return $status; + } + + /** + * 改进版智能预测算法 - 基于统计回归分析 + * @param int $periods 查询最近多少期用于统计分析 + * @param array $weights 各维度权重配置 + * @param string $targetExpect 目标期号(可选,用于验证历史预测成功率) + * @param bool $skipBacktest 是否跳过回测(内部调用时使用) + * @param int $backtestCount 回测期数(默认50) + * @return array {predictions: [], last_special: int, analysis: {}, actual_result: {}, hit_info: {}, backtest: {}} + */ + public function getPredictionV2($periods = 200, $weights = [], $targetExpect = '', $skipBacktest = false, $backtestCount = 50) + { + // 获取号码属性映射 + $num_model = new Num(); + $colorMap = $num_model->column('color', 'num'); + $animalMap = $num_model->column('animal', 'num'); + + // 默认权重配置(改进版) + $defaultWeights = [ + 'omit_regression' => 0.30, // 遗漏回归权重 + 'freq_regression' => 0.25, // 频率回归权重(已合并统计显著性) + 'recent_trend' => 0.20, // 近期趋势权重 + 'zone_balance' => 0.10, // 区域平衡权重 + 'color_balance' => 0.08, // 波色平衡权重 + 'stat_significance' => 0.07 // 统计显著性权重(已合并到freq_regression,保留兼容) + ]; + $weights = array_merge($defaultWeights, $weights); + + // 确定预测基准期和目标期 + $actualResult = null; + $lastSpecial = 0; + $lastExpect = ''; + $cutoffTime = null; + $allHistory = []; + + if ($targetExpect) { + $targetRow = $this->where('expect', $targetExpect)->find(); + if (!$targetRow) { + return ['predictions' => [], 'error' => '期号不存在', 'target_expect' => $targetExpect]; + } + $cutoffTime = $targetRow['openTime']; + $actualResult = [ + 'expect' => (string)$targetRow['expect'], + 'num7' => (int)$targetRow['num7'], + 'color' => $colorMap[$targetRow['num7']] ?? '', + 'animal' => $animalMap[$targetRow['num7']] ?? '', + 'openTime' => $targetRow['openTime'] + ]; + $prevRow = $this->where('openTime', '<', $cutoffTime)->order('openTime', 'desc')->limit(1)->find(); + if (!$prevRow) { + return ['predictions' => [], 'error' => '目标期号之前没有历史数据', 'target_expect' => $targetExpect]; + } + $lastSpecial = (int)$prevRow['num7']; + $lastExpect = (string)$prevRow['expect']; + $allHistory = $this->field('expect,num7,openTime') + ->where('openTime', '<', $cutoffTime) + ->order('openTime', 'desc') + ->limit($periods) + ->select(); + } else { + $latest = $this->field('expect,num7,openTime')->order('openTime', 'desc')->limit(1)->find(); + if (!$latest) { + return ['predictions' => [], 'last_special' => 0, 'analysis' => []]; + } + $lastSpecial = (int)$latest['num7']; + $lastExpect = (string)$latest['expect']; + $allHistory = $this->field('expect,num7,openTime') + ->order('openTime', 'desc') + ->limit($periods) + ->select(); + } + + if (empty($allHistory) || count($allHistory) < 30) { + return ['predictions' => [], 'error' => '历史数据不足(至少需要30期)', 'last_special' => $lastSpecial]; + } + + // 反转为升序(从旧到新) + $historyAsc = array_reverse($allHistory); + + // ====== 1. 遗漏值分析 ====== + // 计算每个号码的当前遗漏期数 + $omitCount = array_fill(1, 49, count($allHistory)); + $periodIdx = 0; + foreach ($allHistory as $row) { + $num = (int)$row['num7']; + if ($num >= 1 && $num <= 49 && $omitCount[$num] === count($allHistory)) { + $omitCount[$num] = $periodIdx; + } + $periodIdx++; + } + + // 计算历史遗漏值分布统计 + $omitHistory = []; + $omitStats = ['avg' => 0, 'max' => 0, 'std' => 0]; + foreach ($allHistory as $row) { + $num = (int)$row['num7']; + // 计算该号码在开奖时的遗漏值 + $tempOmit = array_fill(1, 49, count($allHistory)); + $foundIdx = -1; + $idx = 0; + foreach ($allHistory as $checkRow) { + if ($checkRow['openTime'] === $row['openTime']) { + $foundIdx = $idx; + break; + } + $checkNum = (int)$checkRow['num7']; + if ($checkNum >= 1 && $checkNum <= 49 && $tempOmit[$checkNum] === count($allHistory)) { + $tempOmit[$checkNum] = $idx; + } + $idx++; + } + if ($foundIdx >= 0) { + $omitAtOpen = $tempOmit[$num]; + $omitHistory[] = $omitAtOpen; + } + } + + // 遗漏值统计 + if (count($omitHistory) > 0) { + $omitStats['avg'] = array_sum($omitHistory) / count($omitHistory); + $omitStats['max'] = max($omitHistory); + $omitStats['std'] = $this->_calcStdDev($omitHistory); + } + + // ====== 2. 频率分析 ====== + // 全历史频率 + $freqAll = array_fill(1, 49, 0); + foreach ($allHistory as $row) { + $num = (int)$row['num7']; + if ($num >= 1 && $num <= 49) { + $freqAll[$num]++; + } + } + $expectedFreq = count($allHistory) / 49; + + // 近期频率(最近30期) + $recent30 = array_slice($allHistory, 0, 30); + $freq30 = array_fill(1, 49, 0); + foreach ($recent30 as $row) { + $num = (int)$row['num7']; + if ($num >= 1 && $num <= 49) { + $freq30[$num]++; + } + } + $expectedFreq30 = 30 / 49; + + // 近期频率(最近10期) + $recent10 = array_slice($allHistory, 0, 10); + $freq10 = array_fill(1, 49, 0); + foreach ($recent10 as $row) { + $num = (int)$row['num7']; + if ($num >= 1 && $num <= 49) { + $freq10[$num]++; + } + } + $expectedFreq10 = 10 / 49; + + // ====== 3. 区域分布分析 ====== + $zoneLabels = ['1-10', '11-20', '21-30', '31-40', '41-49']; + $zoneCountAll = array_fill(0, 5, 0); + $zoneCount30 = array_fill(0, 5, 0); + $zoneCount10 = array_fill(0, 5, 0); + + foreach ($allHistory as $row) { + $num = (int)$row['num7']; + $zone = $this->_getZoneIdx($num); + if ($zone >= 0) $zoneCountAll[$zone]++; + } + foreach ($recent30 as $row) { + $num = (int)$row['num7']; + $zone = $this->_getZoneIdx($num); + if ($zone >= 0) $zoneCount30[$zone]++; + } + foreach ($recent10 as $row) { + $num = (int)$row['num7']; + $zone = $this->_getZoneIdx($num); + if ($zone >= 0) $zoneCount10[$zone]++; + } + + // ====== 4. 波色分布分析 ====== + $colorCountAll = ['红' => 0, '蓝' => 0, '绿' => 0]; + $colorCount30 = ['红' => 0, '蓝' => 0, '绿' => 0]; + $colorCount10 = ['红' => 0, '蓝' => 0, '绿' => 0]; + + foreach ($allHistory as $row) { + $num = (int)$row['num7']; + $color = $colorMap[$num] ?? ''; + if (strpos($color, '红') !== false) $colorCountAll['红']++; + elseif (strpos($color, '蓝') !== false) $colorCountAll['蓝']++; + elseif (strpos($color, '绿') !== false) $colorCountAll['绿']++; + } + foreach ($recent30 as $row) { + $num = (int)$row['num7']; + $color = $colorMap[$num] ?? ''; + if (strpos($color, '红') !== false) $colorCount30['红']++; + elseif (strpos($color, '蓝') !== false) $colorCount30['蓝']++; + elseif (strpos($color, '绿') !== false) $colorCount30['绿']++; + } + foreach ($recent10 as $row) { + $num = (int)$row['num7']; + $color = $colorMap[$num] ?? ''; + if (strpos($color, '红') !== false) $colorCount10['红']++; + elseif (strpos($color, '蓝') !== false) $colorCount10['蓝']++; + elseif (strpos($color, '绿') !== false) $colorCount10['绿']++; + } + + // ====== 5. 计算综合得分 ====== + $scores = []; + $analysis = [ + 'last_special' => $lastSpecial, + 'last_expect' => $lastExpect, + 'weights' => $weights, + 'omit_stats' => $omitStats, + 'expected_freq' => round($expectedFreq, 2), + 'zone_expected' => count($allHistory) / 5, + 'color_expected' => count($allHistory) / 3, + 'target_expect' => $targetExpect + ]; + + for ($num = 1; $num <= 49; $num++) { + $totalScore = 0; + $detail = []; + + // === 遗漏回归得分 === + // 遗漏值超过历史平均遗漏值时,回归概率更高 + $omitScore = 0; + $currentOmit = $omitCount[$num]; + $avgOmit = $omitStats['avg']; + $stdOmit = $omitStats['std']; + + if ($avgOmit > 0 && $stdOmit > 0) { + // 使用正态分布模型计算回归概率 + // 遗漏值越偏离均值,回归得分越高 + $zScore = ($currentOmit - $avgOmit) / $stdOmit; + if ($zScore > 0) { + // 遗漏值高于均值,回归概率增加 + $omitScore = min(100, $zScore * 30); // 最高100分 + } else { + // 遗漏值低于均值,回归概率降低 + $omitScore = max(0, 10 + $zScore * 5); // 最低0分 + } + } else { + $zScore = 0; + $omitScore = 50; + } + $detail['omit'] = $currentOmit; + $detail['omit_zscore'] = round($zScore, 2); + $detail['omit_score'] = round($omitScore, 2); + $totalScore += $omitScore * $weights['omit_regression']; + + // === 频率回归得分(已合并统计显著性) === + // 近期频率低于期望频率时,回归概率更高 + $freqScore = 0; + $freqRatio = $freqAll[$num] / $expectedFreq; + $freqRatio30 = $freq30[$num] / $expectedFreq30; + $freqRatio10 = $freq10[$num] / $expectedFreq10; + + // 近期频率低于期望,预测回归 + if ($freqRatio30 < 1) { + $freqScore = (1 - $freqRatio30) * 50; // 低于期望时加分 + } else { + $freqScore = max(0, 20 - ($freqRatio30 - 1) * 20); // 高于期望时减分 + } + + // 结合10期频率的极端情况 + if ($freq10[$num] === 0) { + $freqScore += 30; // 最近10期未出现,额外加分 + } + + // 合并统计显著性:频率偏离期望越大,回归信号越强 + $chiSquare = 0; + if ($expectedFreq > 0) { + $chiSquare = pow($freqAll[$num] - $expectedFreq, 2) / $expectedFreq; + } + if ($freqAll[$num] < $expectedFreq) { + // 频率低于期望(可能回归),加分 + $chiBonus = min(30, $chiSquare * 5); + $freqScore += $chiBonus; + } + + $detail['freq_all'] = $freqAll[$num]; + $detail['freq_30'] = $freq30[$num]; + $detail['freq_10'] = $freq10[$num]; + $detail['freq_ratio_30'] = round($freqRatio30, 2); + $detail['chi_square'] = round($chiSquare, 2); + $detail['freq_score'] = round($freqScore, 2); + $totalScore += $freqScore * $weights['freq_regression']; + + // === 近期趋势得分 === + // 分析最近5期号码的走势趋势 + $trendScore = 0; + $recent5 = array_slice($allHistory, 0, 5); + $recent5Nums = []; + foreach ($recent5 as $row) { + $recent5Nums[] = (int)$row['num7']; + } + // 计算号码与最近5期号码的距离 + $avgDistance = 0; + foreach ($recent5Nums as $rNum) { + $avgDistance += abs($num - $rNum); + } + $avgDistance = $avgDistance / 5; + // 梯度评分:距离适中(15-22)得分最高,越偏离越低 + if ($avgDistance >= 15 && $avgDistance <= 22) { + $trendScore = 80; // 最佳距离区间 + } elseif ($avgDistance >= 10 && $avgDistance < 15) { + $trendScore = 60 + ($avgDistance - 10) * 4; // 60-80 + } elseif ($avgDistance > 22 && $avgDistance <= 30) { + $trendScore = 60 - ($avgDistance - 22) * 3.75; // 60-30 + } elseif ($avgDistance < 10) { + $trendScore = 20 + $avgDistance * 4; // 20-60 + } else { + $trendScore = 10; // 距离过远 + } + $detail['avg_distance'] = round($avgDistance, 2); + $detail['trend_score'] = $trendScore; + $totalScore += $trendScore * $weights['recent_trend']; + + // === 区域平衡得分 === + // 计算号码所在区域的近期失衡程度 + $zone = $this->_getZoneIdx($num); + $zoneExpected30 = 30 / 5; + $zoneRatio = $zoneCount30[$zone] / $zoneExpected30; + $zoneScore = 0; + if ($zoneRatio < 1) { + $zoneScore = (1 - $zoneRatio) * 40; // 区域出现不足,加分 + } else { + $zoneScore = max(0, 20 - ($zoneRatio - 1) * 20); + } + $detail['zone'] = $zoneLabels[$zone]; + $detail['zone_count_30'] = $zoneCount30[$zone]; + $detail['zone_score'] = round($zoneScore, 2); + $totalScore += $zoneScore * $weights['zone_balance']; + + // === 波色平衡得分 === + // 波色只有3种颜色,期望频率更高(30/3=10),波动比区域更小 + // 使用更敏感的系数:波色不足时加分更多 + $color = $colorMap[$num] ?? ''; + $colorKey = ''; + if (strpos($color, '红') !== false) $colorKey = '红'; + elseif (strpos($color, '蓝') !== false) $colorKey = '蓝'; + elseif (strpos($color, '绿') !== false) $colorKey = '绿'; + + $colorScore = 0; + if ($colorKey && isset($colorCount30[$colorKey])) { + $colorExpected30 = 30 / 3; + $colorRatio = $colorCount30[$colorKey] / $colorExpected30; + if ($colorRatio < 1) { + // 波色出现不足,加分(系数50,比区域的40更敏感) + $colorScore = (1 - $colorRatio) * 50; + } else { + // 波色出现过多,减分(系数25,比区域的20更严格) + $colorScore = max(0, 25 - ($colorRatio - 1) * 25); + } + } + $detail['color'] = $colorKey; + $detail['color_count_30'] = $colorCount30[$colorKey] ?? 0; + $detail['color_score'] = round($colorScore, 2); + $totalScore += $colorScore * $weights['color_balance']; + + // 统计显著性已合并到频率回归得分中,此处不再重复计算 + + $scores[$num] = [ + 'num' => $num, + 'score' => round($totalScore, 2), + 'color' => $colorMap[$num] ?? '', + 'animal' => $animalMap[$num] ?? '', + 'detail' => $detail + ]; + } + + // 按得分降序排序 + $predictions = array_values($scores); + usort($predictions, function ($a, $b) { + return $b['score'] - $a['score']; + }); + + // 返回Top 5预测 + $predictions = array_slice($predictions, 0, 5); + + // ====== 6. 历史回测验证 ====== + // 跳过回测以避免递归(回测内部调用此方法时会传入true) + $backtest = $skipBacktest ? null : $this->_runBacktest($periods, $weights, $backtestCount, $cutoffTime); + + // 计算命中情况 + $hitInfo = null; + if ($actualResult) { + $hitRank = -1; + foreach ($predictions as $idx => $p) { + if ($p['num'] === $actualResult['num7']) { + $hitRank = $idx + 1; + break; + } + } + $hitInfo = [ + 'hit' => $hitRank > 0, + 'rank' => $hitRank, + 'actual_num' => $actualResult['num7'], + 'actual_color' => $actualResult['color'], + 'actual_animal' => $actualResult['animal'], + 'actual_expect' => $actualResult['expect'] + ]; + } + + return [ + 'predictions' => $predictions, + 'last_special' => $lastSpecial, + 'last_expect' => $lastExpect, + 'analysis' => $analysis, + 'actual_result' => $actualResult, + 'hit_info' => $hitInfo, + 'backtest' => $backtest + ]; + } + + /** + * 获取号码所在区域索引 + * @param int $num 号码 + * @return int 区域索引(0-4),-1表示无效 + */ + private function _getZoneIdx($num) + { + if ($num < 1 || $num > 49) return -1; + if ($num <= 10) return 0; + if ($num <= 20) return 1; + if ($num <= 30) return 2; + if ($num <= 40) return 3; + return 4; + } + + /** + * 计算标准差 + * @param array $data 数据数组 + * @return float 标准差 + */ + private function _calcStdDev($data) + { + if (count($data) < 2) return 0; + $mean = array_sum($data) / count($data); + $sumSq = 0; + foreach ($data as $val) { + $sumSq += pow($val - $mean, 2); + } + return sqrt($sumSq / count($data)); + } + + /** + * 历史回测验证 + * 使用历史数据验证预测算法的有效性 + * @param int $periods 统计期数 + * @param array $weights 权重配置 + * @param int $testCount 回测次数 + * @param string|null $cutoffTime 截止时间(基于目标期号),null表示使用最新数据 + * @return array {hit_rate: float, avg_rank: float, details: []} + */ + private function _runBacktest($periods, $weights, $testCount = 50, $cutoffTime = null) + { + // 获取足够的历史数据进行回测 + $query = $this->field('expect,num7,openTime'); + if ($cutoffTime) { + $query->where('openTime', '<', $cutoffTime); + } + $totalHistory = $query->order('openTime', 'desc') + ->limit($periods + $testCount + 20) + ->select(); + + if (count($totalHistory) < $periods + $testCount) { + return ['hit_rate' => 0, 'avg_rank' => 0, 'details' => [], 'error' => '数据不足']; + } + + $hits = 0; + $ranks = []; + $details = []; + + // 对最近 testCount 期进行回测 + for ($i = 0; $i < $testCount; $i++) { + $targetRow = $totalHistory[$i]; + $targetExpect = (string)$targetRow['expect']; + $actualNum = (int)$targetRow['num7']; + + // 使用改进版预测算法进行预测(跳过回测避免递归) + $predResult = $this->getPredictionV2($periods, $weights, $targetExpect, true); + + if (isset($predResult['error']) || empty($predResult['predictions'])) { + continue; + } + + // 检查命中情况 + $rank = -1; + foreach ($predResult['predictions'] as $idx => $p) { + if ($p['num'] === $actualNum) { + $rank = $idx + 1; + break; + } + } + + if ($rank > 0) { + $hits++; + $ranks[] = $rank; + } + + // 记录所有回测的详细信息 + $details[] = [ + 'expect' => $targetExpect, + 'actual' => $actualNum, + 'predictions' => array_column($predResult['predictions'], 'num'), + 'hit' => $rank > 0, + 'rank' => $rank + ]; + } + + $hitRate = $testCount > 0 ? round($hits / $testCount * 100, 2) : 0; + $avgRank = count($ranks) > 0 ? round(array_sum($ranks) / count($ranks), 2) : 0; + + return [ + 'hit_rate' => $hitRate, + 'avg_rank' => $avgRank, + 'total_tests' => $testCount, + 'total_hits' => $hits, + 'details' => $details + ]; + } + + /** + * 特码预测算法 V3 - 多维度综合预测 + * 新增维度:转移概率(马尔可夫链)、单双规律、大小规律、走势方向分析 + * + * @param int $periods 统计期数(默认200) + * @param array $weights 权重配置(可选) + * @param string $targetExpect 目标期号(可选,用于验证历史预测成功率) + * @param bool $skipBacktest 是否跳过回测(内部调用时使用) + * @param int $backtestCount 回测期数(默认50) + * @return array {predictions: [], last_special: int, analysis: {}, actual_result: {}, hit_info: {}, backtest: {}} + */ + public function getPredictionV3($periods = 200, $weights = [], $targetExpect = '', $skipBacktest = false, $backtestCount = 50) + { + // 获取号码属性映射 + $num_model = new Num(); + $colorMap = $num_model->column('color', 'num'); + $animalMap = $num_model->column('animal', 'num'); + + // 默认权重配置(V3优化版,新增组合特征维度) + $defaultWeights = [ + 'omit_regression' => 0.18, // 遗漏回归权重 + 'freq_regression' => 0.12, // 频率回归权重 + 'transition_prob' => 0.18, // 转移概率权重 + 'oddeven_balance' => 0.08, // 单双平衡权重 + 'bigsmall_balance' => 0.08, // 大小平衡权重 + 'trend_direction' => 0.14, // 走势方向权重 + 'zone_balance' => 0.04, // 区域平衡权重 + 'color_balance' => 0.04, // 波色平衡权重 + 'combination' => 0.10 // 组合特征权重(新增) + ]; + $weights = array_merge($defaultWeights, $weights); + + // 确定预测基准期和目标期 + $actualResult = null; + $lastSpecial = 0; + $lastExpect = ''; + $cutoffTime = null; + $allHistory = []; + + if ($targetExpect) { + $targetRow = $this->where('expect', $targetExpect)->find(); + if (!$targetRow) { + return ['predictions' => [], 'error' => '期号不存在', 'target_expect' => $targetExpect]; + } + $cutoffTime = $targetRow['openTime']; + $actualResult = [ + 'expect' => (string)$targetRow['expect'], + 'num7' => (int)$targetRow['num7'], + 'color' => $colorMap[$targetRow['num7']] ?? '', + 'animal' => $animalMap[$targetRow['num7']] ?? '', + 'openTime' => $targetRow['openTime'] + ]; + $prevRow = $this->where('openTime', '<', $cutoffTime)->order('openTime', 'desc')->limit(1)->find(); + if (!$prevRow) { + return ['predictions' => [], 'error' => '目标期号之前没有历史数据', 'target_expect' => $targetExpect]; + } + $lastSpecial = (int)$prevRow['num7']; + $lastExpect = (string)$prevRow['expect']; + $allHistory = $this->field('expect,num7,openTime') + ->where('openTime', '<', $cutoffTime) + ->order('openTime', 'desc') + ->limit($periods) + ->select(); + } else { + $latest = $this->field('expect,num7,openTime')->order('openTime', 'desc')->limit(1)->find(); + if (!$latest) { + return ['predictions' => [], 'last_special' => 0, 'analysis' => []]; + } + $lastSpecial = (int)$latest['num7']; + $lastExpect = (string)$latest['expect']; + $allHistory = $this->field('expect,num7,openTime') + ->order('openTime', 'desc') + ->limit($periods) + ->select(); + } + + if (empty($allHistory) || count($allHistory) < 30) { + return ['predictions' => [], 'error' => '历史数据不足(至少需要30期)', 'last_special' => $lastSpecial]; + } + + // 反转为升序(从旧到新) + $historyAsc = array_reverse($allHistory); + + // ====== 1. 遗漏值分析(优化版 O(n))====== + $omitCount = array_fill(1, 49, count($allHistory)); + $omitHistory = []; + $lastAppear = array_fill(1, 49, -1); + + foreach ($historyAsc as $idx => $row) { + $num = (int)$row['num7']; + if ($num >= 1 && $num <= 49) { + // 记录该号码在开奖时的遗漏值 + if ($lastAppear[$num] >= 0) { + $omitHistory[] = $idx - $lastAppear[$num]; + } else { + $omitHistory[] = $idx + 1; + } + $lastAppear[$num] = $idx; + // 更新当前遗漏(从最新位置到现在的距离) + $omitCount[$num] = count($historyAsc) - 1 - $idx; + } + } + + // 历史中未出现的号码,遗漏值设为总期数 + foreach ($omitCount as $n => $v) { + if ($lastAppear[$n] < 0) { + $omitCount[$n] = count($allHistory); + } + } + + $omitStats = ['avg' => 0, 'max' => 0, 'std' => 0]; + if (count($omitHistory) > 0) { + $omitStats['avg'] = array_sum($omitHistory) / count($omitHistory); + $omitStats['max'] = max($omitHistory); + $omitStats['std'] = $this->_calcStdDev($omitHistory); + } + + // ====== 2. 频率分析 ====== + $freqAll = array_fill(1, 49, 0); + foreach ($allHistory as $row) { + $num = (int)$row['num7']; + if ($num >= 1 && $num <= 49) { + $freqAll[$num]++; + } + } + $expectedFreq = count($allHistory) / 49; + + $recent30 = array_slice($allHistory, 0, 30); + $freq30 = array_fill(1, 49, 0); + foreach ($recent30 as $row) { + $num = (int)$row['num7']; + if ($num >= 1 && $num <= 49) { + $freq30[$num]++; + } + } + $expectedFreq30 = 30 / 49; + + $recent10 = array_slice($allHistory, 0, 10); + $freq10 = array_fill(1, 49, 0); + foreach ($recent10 as $row) { + $num = (int)$row['num7']; + if ($num >= 1 && $num <= 49) { + $freq10[$num]++; + } + } + $expectedFreq10 = 10 / 49; + + // ====== 3. 转移概率分析(新增)====== + // 获取转移概率矩阵数据 + $zoneTransition = $this->_getTransitionMatrix($allHistory, 'zone'); + $tailTransition = $this->_getTransitionMatrix($allHistory, 'tail'); + $headTransition = $this->_getTransitionMatrix($allHistory, 'head'); + + // 上期号码的各类属性 + $lastZone = $this->_getZoneIdx($lastSpecial); + $lastTail = $lastSpecial % 10; + $lastHead = $this->_getHeadIdx($lastSpecial); + + // ====== 4. 单双规律分析(新增)====== + $oddevenStats = $this->_analyzeOddEven($allHistory); + + // ====== 5. 大小规律分析(新增)====== + $bigsmallStats = $this->_analyzeBigSmall($allHistory); + + // ====== 6. 走势方向分析(新增)====== + $trendDirection = $this->_analyzeTrendDirection($allHistory); + + // ====== 6.1 动态权重调整(根据走势特征)====== + $weights = $this->_adjustWeightsDynamic($weights, $trendDirection, $omitStats, $oddevenStats, $bigsmallStats); + + // ====== 7. 区域分布分析 ====== + $zoneLabels = ['1-10', '11-20', '21-30', '31-40', '41-49']; + $zoneCount30 = array_fill(0, 5, 0); + foreach ($recent30 as $row) { + $num = (int)$row['num7']; + $zone = $this->_getZoneIdx($num); + if ($zone >= 0) $zoneCount30[$zone]++; + } + + // ====== 8. 波色分布分析 ====== + $colorCount30 = ['红' => 0, '蓝' => 0, '绿' => 0]; + foreach ($recent30 as $row) { + $num = (int)$row['num7']; + $color = $colorMap[$num] ?? ''; + if (strpos($color, '红') !== false) $colorCount30['红']++; + elseif (strpos($color, '蓝') !== false) $colorCount30['蓝']++; + elseif (strpos($color, '绿') !== false) $colorCount30['绿']++; + } + + // ====== 预计算号码属性索引 ====== + $zoneMap = []; + $colorKeyMap = []; + $tailMap = []; + $headMap = []; + $isOddMap = []; + $isBigMap = []; + + for ($n = 1; $n <= 49; $n++) { + $zoneMap[$n] = $this->_getZoneIdx($n); + $tailMap[$n] = $n % 10; + $headMap[$n] = $this->_getHeadIdx($n); + $isOddMap[$n] = $n % 2 === 1; + $isBigMap[$n] = $n >= 25; + $color = $colorMap[$n] ?? ''; + if (strpos($color, '红') !== false) $colorKeyMap[$n] = '红'; + elseif (strpos($color, '蓝') !== false) $colorKeyMap[$n] = '蓝'; + elseif (strpos($color, '绿') !== false) $colorKeyMap[$n] = '绿'; + else $colorKeyMap[$n] = ''; + } + + // ====== 8.1 组合特征分析(新增)====== + $comboStats = $this->_analyzeAttributeCombinations($allHistory, $colorMap, $isOddMap, $isBigMap); + + // ====== 9. 计算综合得分 ====== + $scores = []; + $analysis = [ + 'last_special' => $lastSpecial, + 'last_expect' => $lastExpect, + 'weights' => $weights, + 'weights_adjusted' => true, // 标记权重已动态调整 + 'omit_stats' => $omitStats, + 'oddeven_stats' => $oddevenStats, + 'bigsmall_stats' => $bigsmallStats, + 'trend_direction' => $trendDirection, + 'last_zone' => $zoneLabels[$lastZone] ?? '', + 'last_tail' => $lastTail, + 'last_head' => $lastHead, + 'target_expect' => $targetExpect, + 'combo_stats' => [ + 'oddbig_pct' => $comboStats['oddbig_pct'], + 'oddsmall_pct' => $comboStats['oddsmall_pct'], + 'evenbig_pct' => $comboStats['evenbig_pct'], + 'evensmall_pct' => $comboStats['evensmall_pct'] + ] + ]; + + // 预计算最近5期号码(用于趋势距离) + $recent5Nums = []; + foreach (array_slice($allHistory, 0, 5) as $row) { + $recent5Nums[] = (int)$row['num7']; + } + + for ($num = 1; $num <= 49; $num++) { + $totalScore = 0; + $detail = []; + + // === 遗漏回归得分(使用经验分布版本)=== + $omitScore = $this->_calcOmitScoreEmpirical($omitCount[$num], $omitHistory, $omitStats); + $detail['omit'] = $omitCount[$num]; + $detail['omit_score'] = round($omitScore, 2); + $totalScore += $omitScore * $weights['omit_regression']; + + // === 频率回归得分(使用高级版本)=== + $freqScore = $this->_calcFreqScoreAdvanced($freqAll[$num], $freq30[$num], $freq10[$num], $expectedFreq, $expectedFreq30, $omitHistory, $num); + $detail['freq_all'] = $freqAll[$num]; + $detail['freq_30'] = $freq30[$num]; + $detail['freq_score'] = round($freqScore, 2); + $totalScore += $freqScore * $weights['freq_regression']; + + // === 转移概率得分(新增)=== + $transScore = $this->_calcTransitionScore( + $num, $lastZone, $lastTail, $lastHead, + $zoneTransition, $tailTransition, $headTransition, + $zoneMap, $tailMap, $headMap + ); + $detail['trans_score'] = round($transScore, 2); + $totalScore += $transScore * $weights['transition_prob']; + + // === 单双平衡得分(新增)=== + $oddevenScore = $this->_calcOddEvenScore($num, $oddevenStats, $isOddMap); + $detail['is_odd'] = $isOddMap[$num]; + $detail['oddeven_score'] = round($oddevenScore, 2); + $totalScore += $oddevenScore * $weights['oddeven_balance']; + + // === 大小平衡得分(新增)=== + $bigsmallScore = $this->_calcBigSmallScore($num, $bigsmallStats, $isBigMap); + $detail['is_big'] = $isBigMap[$num]; + $detail['bigsmall_score'] = round($bigsmallScore, 2); + $totalScore += $bigsmallScore * $weights['bigsmall_balance']; + + // === 走势方向得分(新增)=== + $trendScore = $this->_calcTrendDirectionScore($num, $trendDirection, $recent5Nums); + $detail['trend_score'] = round($trendScore, 2); + $totalScore += $trendScore * $weights['trend_direction']; + + // === 区域平衡得分 === + $zone = $zoneMap[$num]; + $zoneExpected30 = 30 / 5; + $zoneRatio = $zoneCount30[$zone] / $zoneExpected30; + $zoneScore = $zoneRatio < 1 ? (1 - $zoneRatio) * 40 : max(0, 20 - ($zoneRatio - 1) * 20); + $detail['zone'] = $zoneLabels[$zone]; + $detail['zone_score'] = round($zoneScore, 2); + $totalScore += $zoneScore * $weights['zone_balance']; + + // === 波色平衡得分 === + $colorKey = $colorKeyMap[$num]; + $colorScore = 0; + if ($colorKey && isset($colorCount30[$colorKey])) { + $colorExpected30 = 30 / 3; + $colorRatio = $colorCount30[$colorKey] / $colorExpected30; + $colorScore = $colorRatio < 1 ? (1 - $colorRatio) * 50 : max(0, 25 - ($colorRatio - 1) * 25); + } + $detail['color'] = $colorKey; + $detail['color_score'] = round($colorScore, 2); + $totalScore += $colorScore * $weights['color_balance']; + + // === 组合特征得分(新增)=== + $recent3Nums = array_slice($recent5Nums, 0, 3); + $comboScore = $this->_calcCombinationScore($num, $comboStats, $lastSpecial, $recent3Nums, $colorMap, $isOddMap, $isBigMap); + $detail['combo_score'] = round($comboScore, 2); + $totalScore += $comboScore * $weights['combination']; + + $scores[$num] = [ + 'num' => $num, + 'score' => round($totalScore, 2), + 'color' => $colorMap[$num] ?? '', + 'animal' => $animalMap[$num] ?? '', + 'detail' => $detail + ]; + } + + // 按得分降序排序 + $predictions = array_values($scores); + usort($predictions, function ($a, $b) { + return $b['score'] - $a['score']; + }); + + // 返回Top 5预测 + $predictions = array_slice($predictions, 0, 5); + + // ====== 10. 历史回测验证 ====== + $backtest = $skipBacktest ? null : $this->_runBacktestV3($periods, $weights, $backtestCount, $cutoffTime); + + // 计算命中情况 + $hitInfo = null; + if ($actualResult) { + $hitRank = -1; + foreach ($predictions as $idx => $p) { + if ($p['num'] === $actualResult['num7']) { + $hitRank = $idx + 1; + break; + } + } + $hitInfo = [ + 'hit' => $hitRank > 0, + 'rank' => $hitRank, + 'actual_num' => $actualResult['num7'], + 'actual_color' => $actualResult['color'], + 'actual_animal' => $actualResult['animal'], + 'actual_expect' => $actualResult['expect'] + ]; + } + + return [ + 'predictions' => $predictions, + 'last_special' => $lastSpecial, + 'last_expect' => $lastExpect, + 'analysis' => $analysis, + 'actual_result' => $actualResult, + 'hit_info' => $hitInfo, + 'backtest' => $backtest + ]; + } + + /** + * 获取号码首号索引 + * @param int $num 号码 + * @return int 首号索引(0-4),-1表示无效 + */ + private function _getHeadIdx($num) + { + if ($num < 1 || $num > 49) return -1; + if ($num <= 9) return 0; + if ($num <= 19) return 1; + if ($num <= 29) return 2; + if ($num <= 39) return 3; + return 4; + } + + /** + * 获取转移概率矩阵(内部方法,优化版) + * @param array $history 历史数据(降序,最新在前) + * @param string $type 类型:zone/tail/head + * @return array {matrix: [], row_totals: [], expected_prob: float} + */ + private function _getTransitionMatrix($history, $type) + { + $historyAsc = array_reverse($history); + $categories = []; + $getIdx = null; + + switch ($type) { + case 'zone': + $categories = ['1-10', '11-20', '21-30', '31-40', '41-49']; + $numCategories = 5; + $getIdx = function ($num) { + if ($num <= 10) return 0; + if ($num <= 20) return 1; + if ($num <= 30) return 2; + if ($num <= 40) return 3; + return 4; + }; + break; + case 'tail': + $categories = ['尾0', '尾1', '尾2', '尾3', '尾4', '尾5', '尾6', '尾7', '尾8', '尾9']; + $numCategories = 10; + $getIdx = function ($num) { return $num % 10; }; + break; + case 'head': + $categories = ['首0', '首1', '首2', '首3', '首4']; + $numCategories = 5; + $getIdx = function ($num) { + if ($num <= 9) return 0; + if ($num <= 19) return 1; + if ($num <= 29) return 2; + if ($num <= 39) return 3; + return 4; + }; + break; + } + + $matrix = array_fill(0, $numCategories, array_fill(0, $numCategories, 0)); + $rowTotals = array_fill(0, $numCategories, 0); + + for ($i = 0; $i < count($historyAsc) - 1; $i++) { + $currentNum = (int)$historyAsc[$i]['num7']; + $nextNum = (int)$historyAsc[$i + 1]['num7']; + if ($currentNum < 1 || $currentNum > 49 || $nextNum < 1 || $nextNum > 49) continue; + + $from = $getIdx($currentNum); + $to = $getIdx($nextNum); + $matrix[$from][$to]++; + $rowTotals[$from]++; + } + + // 计算概率矩阵(使用拉普拉斯平滑处理,避免极端概率) + $probMatrix = array_fill(0, $numCategories, array_fill(0, $numCategories, 0)); + for ($i = 0; $i < $numCategories; $i++) { + // 平滑处理:(count + 1) / (total + categories) + // 避免因数据不足导致概率为0或极端值 + $smoothTotal = $rowTotals[$i] + $numCategories; + for ($j = 0; $j < $numCategories; $j++) { + $probMatrix[$i][$j] = ($matrix[$i][$j] + 1) / $smoothTotal; + } + } + + return [ + 'matrix' => $matrix, + 'prob_matrix' => $probMatrix, + 'row_totals' => $rowTotals, + 'expected_prob' => 1 / $numCategories, + 'categories' => $categories + ]; + } + + /** + * 分析单双规律 + * @param array $history 历史数据(降序) + * @return array {odd_count: int, even_count: int, odd_pct: float, recent_odd_streak: int, recent_even_streak: int, history_streaks: []} + */ + private function _analyzeOddEven($history) + { + $oddCount = 0; + $evenCount = 0; + $streaks = []; // 记录连续单/双的次数 + + $currentStreak = 0; + $lastType = null; + + foreach ($history as $row) { + $num = (int)$row['num7']; + $isOdd = $num % 2 === 1; + + if ($isOdd) { + $oddCount++; + $type = 'odd'; + } else { + $evenCount++; + $type = 'even'; + } + + // 记录连续性 + if ($lastType === null || $lastType !== $type) { + if ($currentStreak > 0) { + $streaks[] = ['type' => $lastType, 'length' => $currentStreak]; + } + $currentStreak = 1; + $lastType = $type; + } else { + $currentStreak++; + } + } + + // 最后一段连续 + if ($currentStreak > 0) { + $streaks[] = ['type' => $lastType, 'length' => $currentStreak]; + } + + // 分析连续规律:连续单/双后反转的概率 + $reverseAfterOddStreak = 0; + $reverseAfterEvenStreak = 0; + $oddStreakCount = 0; + $evenStreakCount = 0; + + for ($i = 0; $i < count($streaks) - 1; $i++) { + if ($streaks[$i]['type'] === 'odd') { + $oddStreakCount++; + if ($streaks[$i + 1]['type'] === 'even') { + $reverseAfterOddStreak++; + } + } else { + $evenStreakCount++; + if ($streaks[$i + 1]['type'] === 'odd') { + $reverseAfterEvenStreak++; + } + } + } + + $total = $oddCount + $evenCount; + $recentStreak = $currentStreak; + $recentType = $lastType; + + return [ + 'odd_count' => $oddCount, + 'even_count' => $evenCount, + 'odd_pct' => $total > 0 ? round($oddCount / $total * 100, 1) : 50, + 'even_pct' => $total > 0 ? round($evenCount / $total * 100, 1) : 50, + 'recent_streak' => $recentStreak, + 'recent_type' => $recentType, + 'reverse_odd_rate' => $oddStreakCount > 0 ? round($reverseAfterOddStreak / $oddStreakCount * 100, 1) : 50, + 'reverse_even_rate' => $evenStreakCount > 0 ? round($reverseAfterEvenStreak / $evenStreakCount * 100, 1) : 50, + 'avg_odd_streak' => $this->_calcAvgStreak($streaks, 'odd'), + 'avg_even_streak' => $this->_calcAvgStreak($streaks, 'even') + ]; + } + + /** + * 分析大小规律 + * @param array $history 历史数据(降序) + * @return array 同单双分析结构 + */ + private function _analyzeBigSmall($history) + { + $bigCount = 0; + $smallCount = 0; + $streaks = []; + + $currentStreak = 0; + $lastType = null; + + foreach ($history as $row) { + $num = (int)$row['num7']; + $isBig = $num >= 25; + + if ($isBig) { + $bigCount++; + $type = 'big'; + } else { + $smallCount++; + $type = 'small'; + } + + if ($lastType === null || $lastType !== $type) { + if ($currentStreak > 0) { + $streaks[] = ['type' => $lastType, 'length' => $currentStreak]; + } + $currentStreak = 1; + $lastType = $type; + } else { + $currentStreak++; + } + } + + if ($currentStreak > 0) { + $streaks[] = ['type' => $lastType, 'length' => $currentStreak]; + } + + // 分析反转概率 + $reverseAfterBigStreak = 0; + $reverseAfterSmallStreak = 0; + $bigStreakCount = 0; + $smallStreakCount = 0; + + for ($i = 0; $i < count($streaks) - 1; $i++) { + if ($streaks[$i]['type'] === 'big') { + $bigStreakCount++; + if ($streaks[$i + 1]['type'] === 'small') { + $reverseAfterBigStreak++; + } + } else { + $smallStreakCount++; + if ($streaks[$i + 1]['type'] === 'big') { + $reverseAfterSmallStreak++; + } + } + } + + $total = $bigCount + $smallCount; + + return [ + 'big_count' => $bigCount, + 'small_count' => $smallCount, + 'big_pct' => $total > 0 ? round($bigCount / $total * 100, 1) : 50, + 'small_pct' => $total > 0 ? round($smallCount / $total * 100, 1) : 50, + 'recent_streak' => $currentStreak, + 'recent_type' => $lastType, + 'reverse_big_rate' => $bigStreakCount > 0 ? round($reverseAfterBigStreak / $bigStreakCount * 100, 1) : 50, + 'reverse_small_rate' => $smallStreakCount > 0 ? round($reverseAfterSmallStreak / $smallStreakCount * 100, 1) : 50, + 'avg_big_streak' => $this->_calcAvgStreak($streaks, 'big'), + 'avg_small_streak' => $this->_calcAvgStreak($streaks, 'small') + ]; + } + + /** + * 计算某类型的平均连续长度 + * @param array $streaks 连续记录 + * @param string $type 类型 + * @return float 平均长度 + */ + private function _calcAvgStreak($streaks, $type) + { + $sum = 0; + $count = 0; + foreach ($streaks as $s) { + if ($s['type'] === $type) { + $sum += $s['length']; + $count++; + } + } + return $count > 0 ? round($sum / $count, 1) : 1; + } + + /** + * 分析走势方向 + * @param array $history 历史数据(降序) + * @return array {trend_type: string, trend_strength: float, recent_nums: [], direction_score: float} + */ + private function _analyzeTrendDirection($history) + { + $recent5 = array_slice($history, 0, 5); + $nums = []; + foreach ($recent5 as $row) { + $nums[] = (int)$row['num7']; + } + + if (count($nums) < 3) { + return ['trend_type' => 'unknown', 'trend_strength' => 0, 'recent_nums' => $nums]; + } + + // 计算走势类型 + $ascCount = 0; + $descCount = 0; + $jumpCount = 0; + + for ($i = 0; $i < count($nums) - 1; $i++) { + $diff = $nums[$i] - $nums[$i + 1]; // 注意:history是降序,所以nums[0]是最新的 + if ($diff > 5) $ascCount++; + elseif ($diff < -5) $descCount++; + else $jumpCount++; + } + + // 判断主要趋势 + $trendType = 'jump'; + $trendStrength = 0; + + if ($ascCount >= 2 && $ascCount > $descCount) { + $trendType = 'ascending'; // 号码在减小(因为history降序) + $trendStrength = $ascCount / (count($nums) - 1); + } elseif ($descCount >= 2 && $descCount > $ascCount) { + $trendType = 'descending'; // 号码在增大 + $trendStrength = $descCount / (count($nums) - 1); + } else { + $trendType = 'jump'; + $trendStrength = $jumpCount / (count($nums) - 1); + } + + // 计算平均变化幅度 + $avgChange = 0; + for ($i = 0; $i < count($nums) - 1; $i++) { + $avgChange += abs($nums[$i] - $nums[$i + 1]); + } + $avgChange = $avgChange / (count($nums) - 1); + + return [ + 'trend_type' => $trendType, + 'trend_strength' => round($trendStrength, 2), + 'avg_change' => round($avgChange, 1), + 'recent_nums' => $nums + ]; + } + + /** + * 动态调整权重(根据当前走势特征) + * @param array $baseWeights 基础权重 + * @param array $trendDirection 走势方向分析结果 + * @param array $omitStats 遗漏统计 + * @param array $oddevenStats 单双统计 + * @param array $bigsmallStats 大小统计 + * @return array 调整后的权重 + */ + private function _adjustWeightsDynamic($baseWeights, $trendDirection, $omitStats, $oddevenStats, $bigsmallStats) + { + $weights = $baseWeights; + $adjustments = []; + + // 1. 强趋势时提高趋势方向权重 + if ($trendDirection['trend_strength'] >= 0.7) { + $adjustments['trend_direction'] = 0.5; // +50% + } elseif ($trendDirection['trend_strength'] >= 0.5) { + $adjustments['trend_direction'] = 0.3; // +30% + } + + // 2. 跳跃趋势时提高转移概率权重 + if ($trendDirection['trend_type'] === 'jump') { + $adjustments['transition_prob'] = 0.2; // +20% + } + + // 3. 长遗漏时提高遗漏回归权重 + $avgOmit = $omitStats['avg']; + $maxOmit = $omitStats['max']; + if ($maxOmit > $avgOmit * 2) { + $adjustments['omit_regression'] = 0.3; // +30% + } + + // 4. 单双连续过长时提高单双权重 + if ($oddevenStats['recent_streak'] >= $oddevenStats['avg_odd_streak'] * 1.5 || + $oddevenStats['recent_streak'] >= $oddevenStats['avg_even_streak'] * 1.5) { + $adjustments['oddeven_balance'] = 0.25; // +25% + } + + // 5. 大小连续过长时提高大小权重 + if ($bigsmallStats['recent_streak'] >= $bigsmallStats['avg_big_streak'] * 1.5 || + $bigsmallStats['recent_streak'] >= $bigsmallStats['avg_small_streak'] * 1.5) { + $adjustments['bigsmall_balance'] = 0.25; // +25% + } + + // 6. 单双比例严重失衡时提高权重 + $oddPct = $oddevenStats['odd_pct']; + if ($oddPct > 60 || $oddPct < 40) { + $adjustments['oddeven_balance'] = max($adjustments['oddeven_balance'] ?? 0, 0.35); + } + + // 7. 大小比例严重失衡时提高权重 + $bigPct = $bigsmallStats['big_pct']; + if ($bigPct > 60 || $bigPct < 40) { + $adjustments['bigsmall_balance'] = max($adjustments['bigsmall_balance'] ?? 0, 0.35); + } + + // 应用调整 + foreach ($adjustments as $key => $multiplier) { + if (isset($weights[$key])) { + $weights[$key] = $weights[$key] * (1 + $multiplier); + } + } + + // 归一化权重(确保总和为1) + $total = array_sum($weights); + if ($total > 0) { + foreach ($weights as $key => $value) { + $weights[$key] = $value / $total; + } + } + + return $weights; + } + + /** + * 计算遗漏回归得分 + * @param int $currentOmit 当前遗漏值 + * @param array $omitStats 遗漏统计 + * @return float 得分 + */ + private function _calcOmitScore($currentOmit, $omitStats) + { + $avgOmit = $omitStats['avg']; + $stdOmit = $omitStats['std']; + + if ($avgOmit <= 0 || $stdOmit <= 0) return 50; + + $zScore = ($currentOmit - $avgOmit) / $stdOmit; + + if ($zScore > 0) { + return min(100, $zScore * 30); + } else { + return max(0, 10 + $zScore * 5); + } + } + + /** + * 计算遗漏回归得分(经验分布版本,基于历史数据而非正态假设) + * @param int $currentOmit 当前遗漏值 + * @param array $omitHistory 历史遗漏值数组 + * @param array $omitStats 遗漏统计(可选备用) + * @return float 得分 + */ + private function _calcOmitScoreEmpirical($currentOmit, $omitHistory, $omitStats = []) + { + if (empty($omitHistory) || count($omitHistory) < 10) { + // 数据不足时回退到正态分布版本 + return $this->_calcOmitScore($currentOmit, $omitStats); + } + + // 构建经验累积分布函数(CDF) + $sortedOmits = $omitHistory; + sort($sortedOmits); + $n = count($sortedOmits); + + // 计算当前遗漏的百分位排名 + $rank = 0; + for ($i = 0; $i < $n; $i++) { + if ($sortedOmits[$i] <= $currentOmit) { + $rank++; + } + } + $percentile = $rank / $n; // 0-1之间 + + // 高百分位(长期遗漏)给予高分,预测回归概率高 + // 使用非线性映射:75%分位开始显著增加得分 + $score = 0; + if ($percentile >= 0.9) { + // 极端遗漏(90%+分位):回归概率极高 + $score = 80 + ($percentile - 0.9) * 200; // 80-100 + } elseif ($percentile >= 0.75) { + // 较长遗漏(75%-90%分位):回归概率较高 + $score = 50 + ($percentile - 0.75) * 120; // 50-80 + } elseif ($percentile >= 0.5) { + // 中等遗漏(50%-75%分位):回归概率中等 + $score = 25 + ($percentile - 0.5) * 100; // 25-50 + } elseif ($percentile >= 0.25) { + // 较短遗漏(25%-50%分位):回归概率较低 + $score = 10 + ($percentile - 0.25) * 60; // 10-25 + } else { + // 短遗漏(0%-25%分位):刚出现不久,回归概率低 + $score = $percentile * 40; // 0-10 + } + + // 结合历史最大遗漏值进行额外评估 + $maxOmit = max($sortedOmits); + if ($currentOmit >= $maxOmit * 0.8) { + // 接近历史最大遗漏,额外加分 + $score += 15; + } + + return min(100, max(0, round($score, 2))); + } + + /** + * 计算频率回归得分 + * @param int $freqAll 全历史频率 + * @param int $freq30 近30期频率 + * @param int $freq10 近10期频率 + * @param float $expectedFreq 期望频率 + * @param float $expectedFreq30 近30期期望频率 + * @return float 得分 + */ + private function _calcFreqScore($freqAll, $freq30, $freq10, $expectedFreq, $expectedFreq30) + { + $freqRatio30 = $freq30 / $expectedFreq30; + + $score = 0; + if ($freqRatio30 < 1) { + $score = (1 - $freqRatio30) * 50; + } else { + $score = max(0, 20 - ($freqRatio30 - 1) * 20); + } + + // 最近10期未出现额外加分 + if ($freq10 === 0) { + $score += 30; + } + + // 全历史频率低于期望 + if ($freqAll < $expectedFreq) { + $chiSquare = pow($freqAll - $expectedFreq, 2) / $expectedFreq; + $score += min(30, $chiSquare * 5); + } + + return $score; + } + + /** + * 计算频率回归得分(高级版,基于历史回归间隔分布) + * @param int $freqAll 全历史频率 + * @param int $freq30 近30期频率 + * @param int $freq10 近10期频率 + * @param float $expectedFreq 期望频率 + * @param float $expectedFreq30 近30期期望频率 + * @param array $omitHistory 历史遗漏值数组(用于分析回归间隔) + * @param int $num 当前号码(用于追踪特定号码的遗漏) + * @return float 得分 + */ + private function _calcFreqScoreAdvanced($freqAll, $freq30, $freq10, $expectedFreq, $expectedFreq30, $omitHistory = [], $num = 0) + { + $score = 0; + + // 基础频率回归得分(与原版相同) + $freqRatio30 = $freq30 / $expectedFreq30; + if ($freqRatio30 < 1) { + $score = (1 - $freqRatio30) * 50; + } else { + $score = max(0, 20 - ($freqRatio30 - 1) * 20); + } + + // 全历史频率低于期望时的卡方加分 + if ($freqAll < $expectedFreq) { + $chiSquare = pow($freqAll - $expectedFreq, 2) / $expectedFreq; + $score += min(30, $chiSquare * 5); + } + + // 高级版:基于遗漏历史判断是否在合理回归窗口 + if (!empty($omitHistory) && count($omitHistory) >= 10) { + // 分析遗漏分布,计算典型回归间隔 + $sortedOmits = $omitHistory; + sort($sortedOmits); + $n = count($sortedOmits); + + // 计算中位数和四分位数 + $medianIdx = floor($n / 2); + $q75Idx = floor($n * 0.75); + $medianOmit = $sortedOmits[$medianIdx]; + $q75Omit = $sortedOmits[$q75Idx]; + + // 当前遗漏值(从omitHistory最后一个元素获取) + $currentOmit = end($omitHistory); + + // 判断是否在回归窗口内 + // 回归窗口定义:遗漏值接近或超过历史中位数时,回归概率增加 + if ($freq10 === 0) { + // 近10期未出现:判断是否在合理回归窗口 + if ($currentOmit >= $medianOmit && $currentOmit <= $q75Omit * 1.5) { + // 在回归窗口内,适度加分 + $score += 25; + } elseif ($currentOmit > $q75Omit * 1.5) { + // 超出典型回归窗口很久,回归概率极高 + $score += 35; + } elseif ($currentOmit >= $medianOmit * 0.8) { + // 接近中位数,回归概率适中 + $score += 15; + } + // 低于中位数时不加分(刚出现不久) + } + } else { + // 数据不足时使用简化逻辑 + if ($freq10 === 0) { + // 近10期未出现,但使用更保守的加分 + $score += 20; + } + } + + return min(100, $score); + } + + /** + * 计算转移概率得分 + * @param int $num 待评分号码 + * @param int $lastZone 上期区域 + * @param int $lastTail 上期尾数 + * @param int $lastHead 上期首号 + * @param array $zoneTrans 区域转移矩阵 + * @param array $tailTrans 尾数转移矩阵 + * @param array $headTrans 首号转移矩阵 + * @param array $zoneMap 号码区域映射 + * @param array $tailMap 号码尾数映射 + * @param array $headMap 号码首号映射 + * @return float 得分 + */ + private function _calcTransitionScore($num, $lastZone, $lastTail, $lastHead, $zoneTrans, $tailTrans, $headTrans, $zoneMap, $tailMap, $headMap) + { + $score = 0; + + // 区域转移概率 + if ($lastZone >= 0 && $lastZone < 5) { + $toZone = $zoneMap[$num]; + $zoneProb = $zoneTrans['prob_matrix'][$lastZone][$toZone] ?? 0; + $expectedZoneProb = $zoneTrans['expected_prob']; + // 高于期望概率加分 + if ($zoneProb > $expectedZoneProb) { + $score += ($zoneProb - $expectedZoneProb) * 100; + } + } + + // 尾数转移概率 + if ($lastTail >= 0 && $lastTail < 10) { + $toTail = $tailMap[$num]; + $tailProb = $tailTrans['prob_matrix'][$lastTail][$toTail] ?? 0; + $expectedTailProb = $tailTrans['expected_prob']; + if ($tailProb > $expectedTailProb) { + $score += ($tailProb - $expectedTailProb) * 80; + } + } + + // 首号转移概率 + if ($lastHead >= 0 && $lastHead < 5) { + $toHead = $headMap[$num]; + $headProb = $headTrans['prob_matrix'][$lastHead][$toHead] ?? 0; + $expectedHeadProb = $headTrans['expected_prob']; + if ($headProb > $expectedHeadProb) { + $score += ($headProb - $expectedHeadProb) * 60; + } + } + + return min(100, $score); + } + + /** + * 计算单双平衡得分 + * @param int $num 待评分号码 + * @param array $stats 单双统计 + * @param array $isOddMap 号码单双映射 + * @return float 得分 + */ + private function _calcOddEvenScore($num, $stats, $isOddMap) + { + $isOdd = $isOddMap[$num]; + $score = 0; + + // 当前连续性 + $recentStreak = $stats['recent_streak']; + $recentType = $stats['recent_type']; + $avgStreak = $isOdd ? $stats['avg_odd_streak'] : $stats['avg_even_streak']; + + // 如果当前连续长度超过平均值,预测反转 + if ($recentStreak >= $avgStreak && $recentStreak >= 2) { + // 当前连续的是单,预测反转到双 + if ($recentType === 'odd' && !$isOdd) { + $score += 40 + ($recentStreak - $avgStreak) * 10; + } + // 当前连续的是双,预测反转到单 + if ($recentType === 'even' && $isOdd) { + $score += 40 + ($recentStreak - $avgStreak) * 10; + } + } + + // 比例失衡回归 + $oddPct = $stats['odd_pct']; + if ($oddPct > 55 && !$isOdd) { + // 单号过多,预测双号回归 + $score += ($oddPct - 50) * 2; + } elseif ($oddPct < 45 && $isOdd) { + // 单号过少,预测单号回归 + $score += (50 - $oddPct) * 2; + } + + return min(100, $score); + } + + /** + * 计算大小平衡得分 + * @param int $num 待评分号码 + * @param array $stats 大小统计 + * @param array $isBigMap 号码大小映射 + * @return float 得分 + */ + private function _calcBigSmallScore($num, $stats, $isBigMap) + { + $isBig = $isBigMap[$num]; + $score = 0; + + $recentStreak = $stats['recent_streak']; + $recentType = $stats['recent_type']; + $avgStreak = $isBig ? $stats['avg_big_streak'] : $stats['avg_small_streak']; + + // 连续性反转预测 + if ($recentStreak >= $avgStreak && $recentStreak >= 2) { + if ($recentType === 'big' && !$isBig) { + $score += 40 + ($recentStreak - $avgStreak) * 10; + } + if ($recentType === 'small' && $isBig) { + $score += 40 + ($recentStreak - $avgStreak) * 10; + } + } + + // 比例失衡回归 + $bigPct = $stats['big_pct']; + if ($bigPct > 55 && !$isBig) { + $score += ($bigPct - 50) * 2; + } elseif ($bigPct < 45 && $isBig) { + $score += (50 - $bigPct) * 2; + } + + return min(100, $score); + } + + /** + * 分析属性组合特征 + * @param array $history 历史数据(降序) + * @param array $colorMap 波色映射 + * @param array $isOddMap 单双映射 + * @param array $isBigMap 大小映射 + * @return array 组合统计结果 + */ + private function _analyzeAttributeCombinations($history, $colorMap, $isOddMap, $isBigMap) + { + // 属性组合统计(单+大、单+小、双+大、双+小) + $oddbigCount = 0; + $oddsmallCount = 0; + $evenbigCount = 0; + $evensmallCount = 0; + + // 波色组合统计 + $colorOddCount = ['红' => 0, '蓝' => 0, '绿' => 0]; + $colorEvenCount = ['红' => 0, '蓝' => 0, '绿' => 0]; + $colorBigCount = ['红' => 0, '蓝' => 0, '绿' => 0]; + $colorSmallCount = ['红' => 0, '蓝' => 0, '绿' => 0]; + + // 相邻号码共现统计(±2范围内) + $adjacentPairs = []; + + // 前3期特征对下期影响统计 + $patternInfluence = []; + + $historyAsc = array_reverse($history); + $n = count($historyAsc); + + for ($i = 0; $i < $n; $i++) { + $num = (int)$historyAsc[$i]['num7']; + if ($num < 1 || $num > 49) continue; + + $isOdd = $isOddMap[$num]; + $isBig = $isBigMap[$num]; + $color = $colorMap[$num] ?? ''; + $colorKey = ''; + if (strpos($color, '红') !== false) $colorKey = '红'; + elseif (strpos($color, '蓝') !== false) $colorKey = '蓝'; + elseif (strpos($color, '绿') !== false) $colorKey = '绿'; + + // 属性组合统计 + if ($isOdd && $isBig) $oddbigCount++; + elseif ($isOdd && !$isBig) $oddsmallCount++; + elseif (!$isOdd && $isBig) $evenbigCount++; + else $evensmallCount++; + + // 波色组合统计 + if ($colorKey) { + if ($isOdd) $colorOddCount[$colorKey]++; + else $colorEvenCount[$colorKey]++; + if ($isBig) $colorBigCount[$colorKey]++; + else $colorSmallCount[$colorKey]++; + } + + // 相邻号码共现统计(与前一期号码的关系) + if ($i > 0) { + $prevNum = (int)$historyAsc[$i - 1]['num7']; + // 记录±2范围内的共现 + if (abs($num - $prevNum) <= 2) { + $pairKey = min($num, $prevNum) . '-' . max($num, $prevNum); + $adjacentPairs[$pairKey] = ($adjacentPairs[$pairKey] ?? 0) + 1; + } + } + + // 前3期特征对下期影响 + if ($i >= 3 && $i < $n) { + // 获取前3期的特征组合 + $prev3Pattern = ''; + for ($j = $i - 3; $j < $i; $j++) { + $prevNum = (int)$historyAsc[$j]['num7']; + $prevOdd = $isOddMap[$prevNum] ? 'O' : 'E'; + $prevBig = $isBigMap[$prevNum] ? 'B' : 'S'; + $prev3Pattern .= $prevOdd . $prevBig; + } + // 下期号码的特征 + $nextOdd = $isOdd ? 'O' : 'E'; + $nextBig = $isBig ? 'B' : 'S'; + $nextPattern = $nextOdd . $nextBig; + + $patternKey = $prev3Pattern . '->' . $nextPattern; + $patternInfluence[$patternKey] = ($patternInfluence[$patternKey] ?? 0) + 1; + } + } + + $total = $n; + return [ + 'oddbig_pct' => $total > 0 ? round($oddbigCount / $total * 100, 1) : 25, + 'oddsmall_pct' => $total > 0 ? round($oddsmallCount / $total * 100, 1) : 25, + 'evenbig_pct' => $total > 0 ? round($evenbigCount / $total * 100, 1) : 25, + 'evensmall_pct' => $total > 0 ? round($evensmallCount / $total * 100, 1) : 25, + 'color_odd' => $colorOddCount, + 'color_even' => $colorEvenCount, + 'color_big' => $colorBigCount, + 'color_small' => $colorSmallCount, + 'adjacent_pairs' => $adjacentPairs, + 'pattern_influence' => $patternInfluence + ]; + } + + /** + * 计算组合特征得分 + * @param int $num 待评分号码 + * @param array $comboStats 组合统计结果 + * @param int $lastNum 上期号码 + * @param array $recent3Nums 最近3期号码 + * @param array $colorMap 波色映射 + * @param array $isOddMap 单双映射 + * @param array $isBigMap 大小映射 + * @return float 得分 + */ + private function _calcCombinationScore($num, $comboStats, $lastNum, $recent3Nums, $colorMap, $isOddMap, $isBigMap) + { + $score = 0; + + $isOdd = $isOddMap[$num]; + $isBig = $isBigMap[$num]; + $color = $colorMap[$num] ?? ''; + $colorKey = ''; + if (strpos($color, '红') !== false) $colorKey = '红'; + elseif (strpos($color, '蓝') !== false) $colorKey = '蓝'; + elseif (strpos($color, '绿') !== false) $colorKey = '绿'; + + // 1. 属性组合平衡得分 + // 期望每个组合约占25% + $expectedPct = 25; + if ($isOdd && $isBig) { + $pct = $comboStats['oddbig_pct']; + if ($pct < $expectedPct) { + $score += ($expectedPct - $pct) * 0.8; + } + } elseif ($isOdd && !$isBig) { + $pct = $comboStats['oddsmall_pct']; + if ($pct < $expectedPct) { + $score += ($expectedPct - $pct) * 0.8; + } + } elseif (!$isOdd && $isBig) { + $pct = $comboStats['evenbig_pct']; + if ($pct < $expectedPct) { + $score += ($expectedPct - $pct) * 0.8; + } + } else { + $pct = $comboStats['evensmall_pct']; + if ($pct < $expectedPct) { + $score += ($expectedPct - $pct) * 0.8; + } + } + + // 2. 波色组合平衡得分 + if ($colorKey) { + $colorOddTotal = array_sum($comboStats['color_odd']); + $colorEvenTotal = array_sum($comboStats['color_even']); + if ($isOdd && $colorOddTotal > 0) { + $colorPct = round($comboStats['color_odd'][$colorKey] / $colorOddTotal * 100, 1); + if ($colorPct < 33) { + $score += (33 - $colorPct) * 0.5; + } + } elseif (!$isOdd && $colorEvenTotal > 0) { + $colorPct = round($comboStats['color_even'][$colorKey] / $colorEvenTotal * 100, 1); + if ($colorPct < 33) { + $score += (33 - $colorPct) * 0.5; + } + } + } + + // 3. 相邻号码关联得分 + if ($lastNum >= 1 && $lastNum <= 49) { + // 检查与上期号码是否在±2范围内 + $distance = abs($num - $lastNum); + if ($distance <= 2) { + $pairKey = min($num, $lastNum) . '-' . max($num, $lastNum); + $pairCount = $comboStats['adjacent_pairs'][$pairKey] ?? 0; + // 历史上相邻共现次数多,加分 + if ($pairCount >= 3) { + $score += min(20, $pairCount * 3); + } + } + // 检查与上期号码±1的号码的共现 + for ($offset = -1; $offset <= 1; $offset++) { + if ($offset === 0) continue; + $adjacentNum = $lastNum + $offset; + if ($adjacentNum >= 1 && $adjacentNum <= 49) { + $pairKey = min($num, $adjacentNum) . '-' . max($num, $adjacentNum); + $pairCount = $comboStats['adjacent_pairs'][$pairKey] ?? 0; + if ($pairCount >= 2) { + $score += min(10, $pairCount * 2); + } + } + } + } + + // 4. 前3期特征对下期影响得分 + if (count($recent3Nums) >= 3) { + // 构建前3期特征模式 + $prev3Pattern = ''; + for ($i = 0; $i < 3; $i++) { + $prevNum = $recent3Nums[$i]; + $prevOdd = $isOddMap[$prevNum] ? 'O' : 'E'; + $prevBig = $isBigMap[$prevNum] ? 'B' : 'S'; + $prev3Pattern .= $prevOdd . $prevBig; + } + // 当前号码的特征 + $nextPattern = ($isOdd ? 'O' : 'E') . ($isBig ? 'B' : 'S'); + $patternKey = $prev3Pattern . '->' . $nextPattern; + + // 检查历史中该模式的出现频率 + $patternCount = $comboStats['pattern_influence'][$patternKey] ?? 0; + $totalPatterns = array_sum($comboStats['pattern_influence']); + if ($totalPatterns > 0 && $patternCount > 0) { + $patternProb = $patternCount / $totalPatterns; + // 高于平均概率加分 + $avgProb = 1 / 16; // 16种可能的nextPattern + if ($patternProb > $avgProb) { + $score += min(25, ($patternProb - $avgProb) * 100); + } + } + } + + return min(100, round($score, 2)); + } + + /** + * 计算走势方向得分(优化版:延续与反转互斥,基于趋势强度动态分配) + * @param int $num 待评分号码 + * @param array $trendDir 走势方向分析结果 + * @param array $recent5Nums 最近5期号码 + * @return float 得分 + */ + private function _calcTrendDirectionScore($num, $trendDir, $recent5Nums) + { + $score = 0; + $trendType = $trendDir['trend_type']; + $trendStrength = $trendDir['trend_strength']; + $avgChange = $trendDir['avg_change']; + + // 最近一期号码 + $lastNum = $recent5Nums[0]; + + // 计算号码与最近一期的距离 + $distance = abs($num - $lastNum); + + // 延续得分与反转得分互斥,根据趋势强度分配权重 + // 强趋势:延续概率高,反转概率低 + // 弱趋势:反转概率相对提高 + $continuationWeight = $trendStrength; // 延续权重随趋势强度增加 + $reversalWeight = max(0, 1 - $trendStrength) * 0.5; // 反转权重随趋势强度减少 + + if ($trendType === 'descending' && $trendStrength >= 0.4) { + // 下降趋势(号码增大方向) + if ($num > $lastNum) { + // 延续趋势得分 + $score += 35 * $continuationWeight; + if ($distance <= $avgChange + 8) { + $score += 15 * $continuationWeight; + } + } else { + // 反转得分(仅在弱趋势时有效) + if ($reversalWeight > 0.1) { + $score += 20 * $reversalWeight; + } + } + } elseif ($trendType === 'ascending' && $trendStrength >= 0.4) { + // 上升趋势(号码减小方向) + if ($num < $lastNum) { + $score += 35 * $continuationWeight; + if ($distance <= $avgChange + 8) { + $score += 15 * $continuationWeight; + } + } else { + if ($reversalWeight > 0.1) { + $score += 20 * $reversalWeight; + } + } + } elseif ($trendType === 'jump') { + // 跳跃趋势:号码跳到另一区间 + if ($distance >= 12 && $distance <= 28) { + $score += 45; + } + // 跳跃趋势中也考虑适度反转 + if ($distance >= 5 && $distance <= 15) { + $score += 20; + } + } + + // 超强趋势连续过长时的反转预警(额外加分,不与上述互斥) + // 检查最近3期是否都在同一方向 + if ($trendStrength >= 0.75 && count($recent5Nums) >= 3) { + $sameDirectionCount = 0; + for ($i = 0; $i < 2; $i++) { + $diff = $recent5Nums[$i] - $recent5Nums[$i + 1]; + if ($trendType === 'descending' && $diff > 5) $sameDirectionCount++; + if ($trendType === 'ascending' && $diff < -5) $sameDirectionCount++; + } + // 连续2期同方向后,反转概率略微增加 + if ($sameDirectionCount >= 2) { + if ($trendType === 'descending' && $num < $lastNum && $distance >= 10) { + $score += 12; + } elseif ($trendType === 'ascending' && $num > $lastNum && $distance >= 10) { + $score += 12; + } + } + } + + // 距离适中得分(基于最近5期的平均距离) + $avgDist = 0; + foreach ($recent5Nums as $rNum) { + $avgDist += abs($num - $rNum); + } + $avgDist = $avgDist / 5; + + if ($avgDist >= 15 && $avgDist <= 22) { + $score += 25; + } elseif ($avgDist >= 10 && $avgDist < 15) { + $score += 15; + } + + return min(100, $score); + } + + /** + * 历史回测验证 V3 + * @param int $periods 统计期数 + * @param array $weights 权重配置 + * @param int $testCount 回测次数 + * @param string|null $cutoffTime 截止时间 + * @return array {hit_rate: float, avg_rank: float, details: []} + */ + private function _runBacktestV3($periods, $weights, $testCount = 50, $cutoffTime = null) + { + $query = $this->field('expect,num7,openTime'); + if ($cutoffTime) { + $query->where('openTime', '<', $cutoffTime); + } + $totalHistory = $query->order('openTime', 'desc') + ->limit($periods + $testCount + 20) + ->select(); + + if (count($totalHistory) < $periods + $testCount) { + return ['hit_rate' => 0, 'avg_rank' => 0, 'details' => [], 'error' => '数据不足']; + } + + $hits = 0; + $ranks = []; + $details = []; + + for ($i = 0; $i < $testCount; $i++) { + $targetRow = $totalHistory[$i]; + $targetExpect = (string)$targetRow['expect']; + $actualNum = (int)$targetRow['num7']; + + $predResult = $this->getPredictionV3($periods, $weights, $targetExpect, true); + + if (isset($predResult['error']) || empty($predResult['predictions'])) { + continue; + } + + $rank = -1; + foreach ($predResult['predictions'] as $idx => $p) { + if ($p['num'] === $actualNum) { + $rank = $idx + 1; + break; + } + } + + if ($rank > 0) { + $hits++; + $ranks[] = $rank; + } + + $details[] = [ + 'expect' => $targetExpect, + 'actual' => $actualNum, + 'predictions' => array_column($predResult['predictions'], 'num'), + 'hit' => $rank > 0, + 'rank' => $rank + ]; + } + + $hitRate = $testCount > 0 ? round($hits / $testCount * 100, 2) : 0; + $avgRank = count($ranks) > 0 ? round(array_sum($ranks) / count($ranks), 2) : 0; + + // 计算新增指标(添加数据量检查) + $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 + ]; + } + + /** + * 计算 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; + } + + /** + * 计算 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; + } + }