column('color', 'num'); $history = $this ->field('expect,num1,num2,num3,num4,num5,num6,num7,openTime') ->order('openTime', 'desc') ->limit($periods) ->select(); if (empty($history)) { return ['expects' => [], 'data' => [], 'colorMap' => []]; } $expects = []; $data = []; foreach ($history as $row) { $expects[] = (string)$row['expect']; if ($type === 'special') { $data[] = ['num7' => (int)$row['num7']]; } else { $row_data = []; for ($i = 1; $i <= 7; $i++) { $row_data['num' . $i] = (int)$row['num' . $i]; } $data[] = $row_data; } } // 反转数组,使最远的数据在左边,最近的数据在右边(从左往右,从远到近) $expects = array_reverse($expects); $data = array_reverse($data); return [ 'expects' => $expects, 'data' => $data, 'colorMap' => $colorMap ]; } /** * 获取冷热号码 * @param int $periods 查询最近多少期 * @param string $type 查询类型 all=全部号码 special=仅特码 * @return array {hot: [], cold: [], all: []} */ public function getHotColdNumbers($periods = 30, $type = 'all') { $num_model = new Num(); $colorMap = $num_model->column('color', 'num'); $animalMap = $num_model->column('animal', 'num'); $history = $this ->field('expect,num1,num2,num3,num4,num5,num6,num7,openTime') ->order('openTime', 'desc') ->limit($periods) ->select(); if (empty($history)) { return ['hot' => [], 'cold' => [], 'all' => []]; } $fields = ($type === 'special') ? ['num7'] : ['num1', 'num2', 'num3', 'num4', 'num5', 'num6', 'num7']; // 统计每个号码的出现次数 $count = array_fill(1, 49, 0); $totalAppearances = 0; foreach ($history as $row) { foreach ($fields as $field) { $num = (int)$row[$field]; if ($num >= 1 && $num <= 49) { $count[$num]++; $totalAppearances++; } } } $all = []; for ($num = 1; $num <= 49; $num++) { $percent = $totalAppearances > 0 ? round($count[$num] / $totalAppearances * 100, 1) : 0; $all[] = [ 'num' => $num, 'count' => $count[$num], 'percent' => $percent, 'color' => $colorMap[$num] ?? '—', 'animal' => $animalMap[$num] ?? '—' ]; } // 按出现次数降序排序 $sorted = $all; usort($sorted, function ($a, $b) { return $b['count'] - $a['count']; }); // 热号: top 10, 冷号: bottom 10 $hot = array_slice($sorted, 0, 10); $cold = array_slice($sorted, -10); $cold = array_reverse($cold); return ['hot' => $hot, 'cold' => $cold, 'all' => $all]; } /** * 计算遗漏号码 * @param int $periods 查询最近多少期 * @param string $type 查询类型 all=全部号码 special=仅特码 * @return array [{num: int, omit: int, color: string}, ...] */ public function getMissingNumbers($periods = 10, $type = 'all') { // 查询最近 $periods 期开奖数据 if ($type === 'special') { $history = $this ->field('expect,num7') ->order('openTime', 'desc') ->limit($periods) ->select(); } else { $history = $this ->field('expect,num1,num2,num3,num4,num5,num6,num7') ->order('openTime', 'desc') ->limit($periods) ->select(); } // 收集最近 $periods 期出现过的号码 $appeared = []; foreach ($history as $row) { if ($type === 'special') { $fields = ['num7']; } else { $fields = ['num1', 'num2', 'num3', 'num4', 'num5', 'num6', 'num7']; } foreach ($fields as $field) { if ($row[$field] !== null && $row[$field] !== '') { $appeared[(int)$row[$field]] = true; } } } // 获取遗漏号码(1-49中未出现的) $missing = []; for ($num = 1; $num <= 49; $num++) { if (!isset($appeared[$num])) { $missing[] = $num; } } // 查询更多历史数据用于计算遗漏期数 if ($type === 'special') { $allHistory = $this ->field('num7') ->order('openTime', 'desc') ->limit(500) ->select(); } else { $allHistory = $this ->field('num1,num2,num3,num4,num5,num6,num7') ->order('openTime', 'desc') ->limit(500) ->select(); } // 查询波色映射 $num_model = new Num(); $colorMap = $num_model->column('color', 'num'); // 计算遗漏期数并组装结果 $result = []; foreach ($missing as $num) { $omitCount = $this->calcOmitCount($num, $allHistory, $type); $result[] = [ 'num' => $num, 'omit' => $omitCount, 'color' => $colorMap[$num] ?? '—' ]; } // 按遗漏期数降序排序 usort($result, function ($a, $b) { return $b['omit'] - $a['omit']; }); return $result; } /** * 计算某个号码的遗漏期数 * @param int $num 号码 * @param array $allHistory 历史数据(已按openTime DESC排序) * @param string $type 查询类型 all=全部号码 special=仅特码 * @return int 遗漏期数 */ private function calcOmitCount($num, $allHistory, $type = 'all') { foreach ($allHistory as $idx => $row) { if ($type === 'special') { if ((int)$row['num7'] === $num) { return $idx; } } else { for ($i = 1; $i <= 7; $i++) { if ((int)$row['num' . $i] === $num) { return $idx; } } } } return count($allHistory); } /** * 波色分析 */ public function getColorWaveAnalysis($periods = 30, $type = 'all') { $num_model = new Num(); $colorMap = $num_model->column('color', 'num'); $history = $this->field('expect,num1,num2,num3,num4,num5,num6,num7')->order('openTime', 'desc')->limit($periods)->select(); if (empty($history)) return ['red' => 0, 'blue' => 0, 'green' => 0, 'red_pct' => 0, 'blue_pct' => 0, 'green_pct' => 0, 'details' => []]; $fields = ($type === 'special') ? ['num7'] : ['num1','num2','num3','num4','num5','num6','num7']; $colors = ['红' => 0, '蓝' => 0, '绿' => 0]; $total = 0; foreach ($history as $row) { foreach ($fields as $f) { $num = (int)$row[$f]; $color = $colorMap[$num] ?? ''; if (strpos($color, '红') !== false) { $colors['红']++; $total++; } elseif (strpos($color, '蓝') !== false) { $colors['蓝']++; $total++; } elseif (strpos($color, '绿') !== false) { $colors['绿']++; $total++; } } } return [ 'red' => $colors['红'], 'blue' => $colors['蓝'], 'green' => $colors['绿'], 'red_pct' => $total ? round($colors['红']/$total*100,1) : 0, 'blue_pct' => $total ? round($colors['蓝']/$total*100,1) : 0, 'green_pct' => $total ? round($colors['绿']/$total*100,1) : 0, 'total' => $total ]; } /** * 生肖分析 */ public function getZodiacAnalysis($periods = 30, $type = 'all') { $num_model = new Num(); $animalMap = $num_model->column('animal', 'num'); $colorMap = $num_model->column('color', 'num'); $history = $this->field('expect,num1,num2,num3,num4,num5,num6,num7')->order('openTime', 'desc')->limit($periods)->select(); if (empty($history)) return ['list' => []]; $fields = ($type === 'special') ? ['num7'] : ['num1','num2','num3','num4','num5','num6','num7']; $counts = []; foreach ($history as $row) { foreach ($fields as $f) { $num = (int)$row[$f]; $animal = $animalMap[$num] ?? '未知'; if (!isset($counts[$animal])) $counts[$animal] = ['animal' => $animal, 'count' => 0, 'color' => $colorMap[$num] ?? '—']; $counts[$animal]['count']++; } } $list = array_values($counts); usort($list, function ($a, $b) { return $b['count'] - $a['count']; }); $total = array_sum(array_column($list, 'count')); foreach ($list as &$item) { $item['percent'] = $total ? round($item['count']/$total*100, 1) : 0; } return ['list' => $list]; } /** * 奇偶分析 */ public function getOddEvenAnalysis($periods = 30, $type = 'all') { $history = $this->field('expect,num1,num2,num3,num4,num5,num6,num7')->order('openTime', 'desc')->limit($periods)->select(); if (empty($history)) return ['odd' => 0, 'even' => 0, 'odd_pct' => 0, 'even_pct' => 0, 'per_period' => []]; $fields = ($type === 'special') ? ['num7'] : ['num1','num2','num3','num4','num5','num6','num7']; $odd = 0; $even = 0; $perPeriod = []; foreach ($history as $row) { $p_odd = 0; $p_even = 0; foreach ($fields as $f) { $num = (int)$row[$f]; if ($num % 2 == 0) { $even++; $p_even++; } else { $odd++; $p_odd++; } } $perPeriod[] = ['expect' => $row['expect'], 'odd' => $p_odd, 'even' => $p_even]; } $total = $odd + $even; return [ 'odd' => $odd, 'even' => $even, 'odd_pct' => $total ? round($odd/$total*100, 1) : 0, 'even_pct' => $total ? round($even/$total*100, 1) : 0, 'per_period' => $perPeriod ]; } /** * 大小分析(1-24为小,25-49为大) */ public function getBigSmallAnalysis($periods = 30, $type = 'all') { $history = $this->field('expect,num1,num2,num3,num4,num5,num6,num7')->order('openTime', 'desc')->limit($periods)->select(); if (empty($history)) return ['big' => 0, 'small' => 0, 'big_pct' => 0, 'small_pct' => 0, 'per_period' => []]; $fields = ($type === 'special') ? ['num7'] : ['num1','num2','num3','num4','num5','num6','num7']; $big = 0; $small = 0; $perPeriod = []; foreach ($history as $row) { $p_big = 0; $p_small = 0; foreach ($fields as $f) { $num = (int)$row[$f]; if ($num >= 25) { $big++; $p_big++; } else { $small++; $p_small++; } } $perPeriod[] = ['expect' => $row['expect'], 'big' => $p_big, 'small' => $p_small]; } $total = $big + $small; return [ 'big' => $big, 'small' => $small, 'big_pct' => $total ? round($big/$total*100, 1) : 0, 'small_pct' => $total ? round($small/$total*100, 1) : 0, 'per_period' => $perPeriod ]; } /** * 特码走势 */ public function getSpecialTrend($periods = 30) { $num_model = new Num(); $colorMap = $num_model->column('color', 'num'); // 先取最近 $periods 条数据 $history = $this->field('expect,num7,openTime')->order('openTime', 'desc')->limit($periods)->select(); if (empty($history)) return ['expects' => [], 'specials' => [], 'colors' => []]; // 反转,使数据从左到右为从远到近 $history = array_reverse($history); $expects = []; $specials = []; $colors = []; foreach ($history as $row) { $expects[] = (string)$row['expect']; $num = (int)$row['num7']; $specials[] = $num; $colors[] = $colorMap[$num] ?? ''; } return ['expects' => $expects, 'specials' => $specials, 'colors' => $colors]; } /** * 连号分析 */ public function getConsecutiveNumbers($periods = 30) { $history = $this->field('expect,num1,num2,num3,num4,num5,num6,num7')->order('openTime', 'desc')->limit($periods)->select(); if (empty($history)) return ['pairs' => [], 'triples' => []]; $pairCount = []; $tripleCount = []; foreach ($history as $row) { $nums = []; for ($i = 1; $i <= 7; $i++) $nums[] = (int)$row['num' . $i]; sort($nums); // 找连号对 for ($i = 0; $i < count($nums) - 1; $i++) { if ($nums[$i + 1] - $nums[$i] === 1) { $pair = $nums[$i] . '-' . $nums[$i + 1]; $pairCount[$pair] = isset($pairCount[$pair]) ? $pairCount[$pair] + 1 : 1; } } // 找连号三连 for ($i = 0; $i < count($nums) - 2; $i++) { if ($nums[$i + 1] - $nums[$i] === 1 && $nums[$i + 2] - $nums[$i + 1] === 1) { $triple = $nums[$i] . '-' . $nums[$i + 1] . '-' . $nums[$i + 2]; $tripleCount[$triple] = isset($tripleCount[$triple]) ? $tripleCount[$triple] + 1 : 1; } } } arsort($pairCount); arsort($tripleCount); return ['pairs' => $pairCount, 'triples' => $tripleCount]; } /** * 尾数分析 */ public function getTailNumbers($periods = 30, $type = 'all') { $num_model = new Num(); $colorMap = $num_model->column('color', 'num'); $animalMap = $num_model->column('animal', 'num'); $history = $this->field('expect,num1,num2,num3,num4,num5,num6,num7')->order('openTime', 'desc')->limit($periods)->select(); if (empty($history)) return ['tails' => [], 'all' => []]; $fields = ($type === 'special') ? ['num7'] : ['num1','num2','num3','num4','num5','num6','num7']; $tailCount = array_fill(0, 10, 0); $all = []; foreach ($history as $row) { foreach ($fields as $f) { $num = (int)$row[$f]; $tail = $num % 10; $tailCount[$tail]++; } } $total = array_sum($tailCount); for ($t = 0; $t <= 9; $t++) { $all[] = ['tail' => $t, 'count' => $tailCount[$t], 'percent' => $total ? round($tailCount[$t]/$total*100, 1) : 0]; } usort($all, function ($a, $b) { return $b['count'] - $a['count']; }); return ['all' => $all]; } /** * 综合统计面板 */ public function getDashboardData($periods = 30, $type = 'all') { return [ 'hotcold' => $this->getHotColdNumbers($periods, 'special'), 'colorwave' => $this->getColorWaveAnalysis($periods, 'special'), 'zodiac' => $this->getZodiacAnalysis($periods, 'special'), 'oddeven' => $this->getOddEvenAnalysis($periods, 'special'), 'bigsmall' => $this->getBigSmallAnalysis($periods, 'special'), 'special' => $this->getSpecialTrend($periods), 'tailnumbers' => $this->getTailNumbers($periods, 'special'), 'heatmap' => $this->getSpecialHeatmap($periods), 'zonetransition' => $this->getZoneTransition($periods), 'colorwavetransition' => $this->getColorWaveTransition($periods), 'zonetocolortransition' => $this->getZoneToZoneColor($periods), 'zodiactransition' => $this->getZodiacTransition($periods), 'tailnumbertransition' => $this->getTailNumberTransition($periods), 'headnumbertransition' => $this->getHeadNumberTransition($periods) ]; } /** * 批量查询所有期号特码相对于前N期的冷热状态 * @param int $lookback 向前推算期数 * @param int $limit 查询总期数(从最新往前取) * @return array [{expect, specialNum, count, avgCount, status, rank, totalPeriods}, ...] */ public function getSpecialHotColdList($lookback = 30, $limit = 100) { // 查询最近 $limit 期数据,按时间倒序(最新在前) $history = $this ->field('expect,num7,openTime') ->order('openTime', 'desc') ->limit($limit) ->select(); if (empty($history)) { return []; } // 查询更多历史数据用于统计 $allHistory = $this ->field('expect,num7,openTime') ->order('openTime', 'desc') ->limit($limit + 200) ->select(); // 按openTime排序(确保顺序) $historySorted = []; foreach ($history as $row) { $historySorted[] = $row; } $allHistorySorted = []; foreach ($allHistory as $row) { $allHistorySorted[] = $row; } $result = []; // 对每一期,计算它前面lookback期的冷热状态 for ($i = 0; $i < count($historySorted); $i++) { $row = $historySorted[$i]; $specialNum = (int)$row['num7']; // 找到该期在全量数据中的位置 $targetTime = $row['openTime']; $count = array_fill(1, 49, 0); $periodCount = 0; // 往前统计lookback期(跳过该期本身) for ($j = 0; $j < count($allHistorySorted); $j++) { $checkRow = $allHistorySorted[$j]; $checkTime = $checkRow['openTime']; // 只统计比该期更早的数据 if ($checkTime >= $targetTime) { continue; } if ($periodCount >= $lookback) { break; } $num = (int)$checkRow['num7']; if ($num >= 1 && $num <= 49) { $count[$num]++; } $periodCount++; } if ($periodCount === 0) { $result[] = [ 'expect' => (string)$row['expect'], 'specialNum' => $specialNum, 'count' => 0, 'avgCount' => 0, 'status' => 'unknown', 'rank' => 0 ]; continue; } $targetCount = $count[$specialNum]; $avgCount = $periodCount / 49; $status = 'normal'; if ($avgCount > 0) { if ($targetCount > $avgCount * 1.5) { $status = 'hot'; } elseif ($targetCount < $avgCount * 0.5) { $status = 'cold'; } } // 计算排名 $sorted = []; for ($num = 1; $num <= 49; $num++) { $sorted[] = ['num' => $num, 'count' => $count[$num]]; } usort($sorted, function ($a, $b) { return $b['count'] - $a['count']; }); $rank = 0; foreach ($sorted as $idx => $item) { if ($item['num'] === $specialNum) { $rank = $idx + 1; break; } } $result[] = [ 'expect' => (string)$row['expect'], 'specialNum' => $specialNum, 'count' => $targetCount, 'avgCount' => round($avgCount, 2), 'status' => $status, 'rank' => $rank ]; } // 基于最新一期(result[0])的lookback窗口,计算当前所有49个号码的冷热状态 $current = $this->_computeCurrentHotCold($allHistorySorted, $historySorted[0], $lookback); return [ 'list' => $result, 'current' => $current ]; } /** * 基于指定期的lookback窗口,计算所有49个号码的冷热状态 * @param array $allHistorySorted 全量历史数据 * @param mixed $targetRow 目标期数据 * @param int $lookback 向前期数 * @return array {hot: [{num, count}], cold: [{num, count}], warm: [{num, count}]} */ private function _computeCurrentHotCold($allHistorySorted, $targetRow, $lookback) { $targetTime = $targetRow['openTime']; $count = array_fill(1, 49, 0); $periodCount = 0; for ($j = 0; $j < count($allHistorySorted); $j++) { $checkRow = $allHistorySorted[$j]; $checkTime = $checkRow['openTime']; if ($checkTime >= $targetTime) { continue; } if ($periodCount >= $lookback) { break; } $num = (int)$checkRow['num7']; if ($num >= 1 && $num <= 49) { $count[$num]++; } $periodCount++; } $avgCount = $periodCount > 0 ? $periodCount / 49 : 0; $hot = []; $cold = []; $warm = []; for ($num = 1; $num <= 49; $num++) { $status = 'warm'; if ($avgCount > 0) { if ($count[$num] > $avgCount * 1.5) { $status = 'hot'; } elseif ($count[$num] < $avgCount * 0.5) { $status = 'cold'; } } $item = ['num' => $num, 'count' => $count[$num]]; if ($status === 'hot') { $hot[] = $item; } elseif ($status === 'cold') { $cold[] = $item; } else { $warm[] = $item; } } // 热号按次数降序,冷号按次数升序 usort($hot, function ($a, $b) { return $b['count'] - $a['count']; }); usort($cold, function ($a, $b) { return $a['count'] - $b['count']; }); usort($warm, function ($a, $b) { return $b['count'] - $a['count']; }); return ['hot' => $hot, 'cold' => $cold, 'warm' => $warm]; } /** * 区域转移概率统计 * 将1-49分为5个区域,统计特码从一个区域转移到另一个区域的概率 * 区域1: 1-10, 区域2: 11-20, 区域3: 21-30, 区域4: 31-40, 区域5: 41-49 * @param int $periods 查询最近多少期 * @return array {zones: [], matrix: [], probabilities: [], total_transitions: int} */ public function getZoneTransition($periods = 100) { $history = $this ->field('expect,num7,openTime') ->order('openTime', 'desc') ->limit($periods) ->select(); if (empty($history) || count($history) < 2) { return ['zones' => ['1-10','11-20','21-30','31-40','41-49'], 'matrix' => [], 'probabilities' => [], 'total_transitions' => 0]; } // 反转为升序,从旧到新 $history = array_reverse($history); $zoneLabels = ['1-10', '11-20', '21-30', '31-40', '41-49']; $matrix = array_fill(0, 5, array_fill(0, 5, 0)); $rowTotals = array_fill(0, 5, 0); $getZone = function ($num) { if ($num <= 10) return 0; if ($num <= 20) return 1; if ($num <= 30) return 2; if ($num <= 40) return 3; return 4; }; $totalTransitions = 0; for ($i = 0; $i < count($history) - 1; $i++) { $currentNum = (int)$history[$i]['num7']; $nextNum = (int)$history[$i + 1]['num7']; if ($currentNum < 1 || $currentNum > 49 || $nextNum < 1 || $nextNum > 49) continue; $from = $getZone($currentNum); $to = $getZone($nextNum); $matrix[$from][$to]++; $rowTotals[$from]++; $totalTransitions++; } // 计算概率矩阵 $probabilities = array_fill(0, 5, array_fill(0, 5, 0)); for ($i = 0; $i < 5; $i++) { if ($rowTotals[$i] > 0) { for ($j = 0; $j < 5; $j++) { $probabilities[$i][$j] = round($matrix[$i][$j] / $rowTotals[$i] * 100, 1); } } } return [ 'zones' => $zoneLabels, 'matrix' => $matrix, 'probabilities' => $probabilities, 'row_totals' => $rowTotals, 'total_transitions' => $totalTransitions ]; } /** * 波色转移概率统计 * 统计特码从一种波色转移到另一种波色的概率(红/蓝/绿) * @param int $periods 查询最近多少期 * @return array {colors: [], matrix: [], probabilities: [], row_totals: [], total_transitions: int} */ public function getColorWaveTransition($periods = 100) { $history = $this ->field('expect,num7,openTime') ->order('openTime', 'desc') ->limit($periods) ->select(); if (empty($history) || count($history) < 2) { return ['colors' => ['红波','蓝波','绿波'], 'matrix' => [], 'probabilities' => [], 'row_totals' => [], 'total_transitions' => 0]; } $history = array_reverse($history); $num_model = new Num(); $colorMap = $num_model->column('color', 'num'); $colorLabels = ['红波', '蓝波', '绿波']; $matrix = array_fill(0, 3, array_fill(0, 3, 0)); $rowTotals = array_fill(0, 3, 0); $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; }; $totalTransitions = 0; for ($i = 0; $i < count($history) - 1; $i++) { $currentNum = (int)$history[$i]['num7']; $nextNum = (int)$history[$i + 1]['num7']; $from = $getColorIdx($currentNum); $to = $getColorIdx($nextNum); if ($from < 0 || $to < 0) continue; $matrix[$from][$to]++; $rowTotals[$from]++; $totalTransitions++; } $probabilities = array_fill(0, 3, array_fill(0, 3, 0)); for ($i = 0; $i < 3; $i++) { if ($rowTotals[$i] > 0) { for ($j = 0; $j < 3; $j++) { $probabilities[$i][$j] = round($matrix[$i][$j] / $rowTotals[$i] * 100, 1); } } } return [ 'colors' => $colorLabels, 'matrix' => $matrix, 'probabilities' => $probabilities, 'row_totals' => $rowTotals, 'total_transitions' => $totalTransitions ]; } /** * 生肖转移概率统计 * 统计特码从一种生肖转移到另一种生肖的概率(12生肖) * @param int $periods 查询最近多少期 * @return array {animals: [], matrix: [], probabilities: [], row_totals: [], total_transitions: int} */ public function getZodiacTransition($periods = 100) { $history = $this ->field('expect,num7,openTime') ->order('openTime', 'desc') ->limit($periods) ->select(); if (empty($history) || count($history) < 2) { return ['animals' => ['鼠','牛','虎','兔','龙','蛇','马','羊','猴','鸡','狗','猪'], 'matrix' => [], 'probabilities' => [], 'row_totals' => [], 'total_transitions' => 0]; } $history = array_reverse($history); $num_model = new Num(); $animalMap = $num_model->column('animal', 'num'); $animalLabels = ['鼠', '牛', '虎', '兔', '龙', '蛇', '马', '羊', '猴', '鸡', '狗', '猪']; $numAnimals = 12; $matrix = array_fill(0, $numAnimals, array_fill(0, $numAnimals, 0)); $rowTotals = array_fill(0, $numAnimals, 0); $getAnimalIdx = function ($num) use ($animalMap, $animalLabels) { $animal = $animalMap[$num] ?? ''; $idx = array_search($animal, $animalLabels); return $idx === false ? -1 : $idx; }; $totalTransitions = 0; for ($i = 0; $i < count($history) - 1; $i++) { $currentNum = (int)$history[$i]['num7']; $nextNum = (int)$history[$i + 1]['num7']; $from = $getAnimalIdx($currentNum); $to = $getAnimalIdx($nextNum); if ($from < 0 || $to < 0) continue; $matrix[$from][$to]++; $rowTotals[$from]++; $totalTransitions++; } $probabilities = array_fill(0, $numAnimals, array_fill(0, $numAnimals, 0)); for ($i = 0; $i < $numAnimals; $i++) { if ($rowTotals[$i] > 0) { for ($j = 0; $j < $numAnimals; $j++) { $probabilities[$i][$j] = round($matrix[$i][$j] / $rowTotals[$i] * 100, 1); } } } return [ 'animals' => $animalLabels, 'matrix' => $matrix, 'probabilities' => $probabilities, 'row_totals' => $rowTotals, 'total_transitions' => $totalTransitions ]; } /** * 尾号转移概率统计 * 统计特码从一个尾号转移到另一个尾号的概率(尾号0-9) * @param int $periods 查询最近多少期 * @return array {tails: [], matrix: [], probabilities: [], row_totals: [], total_transitions: int} */ public function getTailNumberTransition($periods = 100) { $history = $this ->field('expect,num7,openTime') ->order('openTime', 'desc') ->limit($periods) ->select(); if (empty($history) || count($history) < 2) { return ['tails' => ['尾0','尾1','尾2','尾3','尾4','尾5','尾6','尾7','尾8','尾9'], 'matrix' => [], 'probabilities' => [], 'row_totals' => [], 'total_transitions' => 0]; } $history = array_reverse($history); $tailLabels = ['尾0', '尾1', '尾2', '尾3', '尾4', '尾5', '尾6', '尾7', '尾8', '尾9']; $numTails = 10; $matrix = array_fill(0, $numTails, array_fill(0, $numTails, 0)); $rowTotals = array_fill(0, $numTails, 0); $getTail = function ($num) { return $num % 10; }; $totalTransitions = 0; for ($i = 0; $i < count($history) - 1; $i++) { $currentNum = (int)$history[$i]['num7']; $nextNum = (int)$history[$i + 1]['num7']; if ($currentNum < 1 || $currentNum > 49 || $nextNum < 1 || $nextNum > 49) continue; $from = $getTail($currentNum); $to = $getTail($nextNum); $matrix[$from][$to]++; $rowTotals[$from]++; $totalTransitions++; } $probabilities = array_fill(0, $numTails, array_fill(0, $numTails, 0)); for ($i = 0; $i < $numTails; $i++) { if ($rowTotals[$i] > 0) { for ($j = 0; $j < $numTails; $j++) { $probabilities[$i][$j] = round($matrix[$i][$j] / $rowTotals[$i] * 100, 1); } } } return [ 'tails' => $tailLabels, 'matrix' => $matrix, 'probabilities' => $probabilities, 'row_totals' => $rowTotals, 'total_transitions' => $totalTransitions ]; } /** * 首号转移概率统计 * 统计特码从一个首号转移到另一个首号的概率(首号0-4) * 首号定义:1-9首号=0,10-19首号=1,20-29首号=2,30-39首号=3,40-49首号=4 * @param int $periods 查询最近多少期 * @return array {heads: [], matrix: [], probabilities: [], row_totals: [], total_transitions: int} */ public function getHeadNumberTransition($periods = 100) { $history = $this ->field('expect,num7,openTime') ->order('openTime', 'desc') ->limit($periods) ->select(); if (empty($history) || count($history) < 2) { return ['heads' => ['首号0','首号1','首号2','首号3','首号4'], 'matrix' => [], 'probabilities' => [], 'row_totals' => [], 'total_transitions' => 0]; } $history = array_reverse($history); $headLabels = ['首号0', '首号1', '首号2', '首号3', '首号4']; $numHeads = 5; $matrix = array_fill(0, $numHeads, array_fill(0, $numHeads, 0)); $rowTotals = array_fill(0, $numHeads, 0); $getHead = function ($num) { return intval(floor($num / 10)); }; $totalTransitions = 0; for ($i = 0; $i < count($history) - 1; $i++) { $currentNum = (int)$history[$i]['num7']; $nextNum = (int)$history[$i + 1]['num7']; if ($currentNum < 1 || $currentNum > 49 || $nextNum < 1 || $nextNum > 49) continue; $from = $getHead($currentNum); $to = $getHead($nextNum); $matrix[$from][$to]++; $rowTotals[$from]++; $totalTransitions++; } $probabilities = array_fill(0, $numHeads, array_fill(0, $numHeads, 0)); for ($i = 0; $i < $numHeads; $i++) { if ($rowTotals[$i] > 0) { for ($j = 0; $j < $numHeads; $j++) { $probabilities[$i][$j] = round($matrix[$i][$j] / $rowTotals[$i] * 100, 1); } } } return [ 'heads' => $headLabels, 'matrix' => $matrix, 'probabilities' => $probabilities, 'row_totals' => $rowTotals, 'total_transitions' => $totalTransitions ]; } /** * 尾首概率:下一期尾数/首位与前一期相同的概率 * 统计相邻两期特码的尾数(num%10)和首位(floor(num/10))变化 * @param int $periods 查询最近多少期 * @return array {tailProb: float, headProb: float, tailSameDetail: array, headSameDetail: array, totalTransitions: int, periodCount: int} */ public function getTailHeadProbability($periods = 100) { $history = $this ->field('expect,num7,openTime') ->order('openTime', 'desc') ->limit($periods) ->select(); if (empty($history) || count($history) < 2) { return ['tailProb' => 0, 'headProb' => 0, 'tailSameDetail' => [], 'headSameDetail' => [], 'totalTransitions' => 0, 'periodCount' => 0]; } // 按时间升序排列(旧→新) $history = array_reverse($history); $tailSame = 0; $headSame = 0; $totalTransitions = 0; $tailSameDetail = []; // 尾数相同明细: key为尾数, value为次数 $headSameDetail = []; // 首位相同明细: key为首数, value为次数 for ($i = 0; $i < count($history) - 1; $i++) { $currentNum = (int)$history[$i]['num7']; $nextNum = (int)$history[$i + 1]['num7']; if ($currentNum < 1 || $currentNum > 49 || $nextNum < 1 || $nextNum > 49) { continue; } $totalTransitions++; $currentTail = $currentNum % 10; $nextTail = $nextNum % 10; $currentHead = intval(floor($currentNum / 10)); $nextHead = intval(floor($nextNum / 10)); // 尾数相同(个位相同) if ($currentTail === $nextTail) { $tailSame++; $key = $currentTail; if (!isset($tailSameDetail[$key])) { $tailSameDetail[$key] = 0; } $tailSameDetail[$key]++; } // 首位相同(十位相同) if ($currentHead === $nextHead) { $headSame++; $key = $currentHead; if (!isset($headSameDetail[$key])) { $headSameDetail[$key] = 0; } $headSameDetail[$key]++; } } // 排序:尾数按0-9排序,首位按0-4排序 $tailResult = []; for ($t = 0; $t <= 9; $t++) { if (isset($tailSameDetail[$t])) { $tailResult[] = ['tail' => $t, 'count' => $tailSameDetail[$t]]; } } $headResult = []; for ($h = 0; $h <= 4; $h++) { if (isset($headSameDetail[$h])) { $headResult[] = ['head' => $h, 'count' => $headSameDetail[$h]]; } } return [ 'tailProb' => $totalTransitions > 0 ? round($tailSame / $totalTransitions * 100, 2) : 0, 'headProb' => $totalTransitions > 0 ? round($headSame / $totalTransitions * 100, 2) : 0, 'tailSameDetail' => $tailResult, 'headSameDetail' => $headResult, 'totalTransitions' => $totalTransitions, 'periodCount' => count($history) ]; } /** * 区域→区域转移矩阵,单元格内显示波色概率 * 统计上一期特码所在区域后,下一期特码在各区域的分布,以及每个区域内的波色占比 * @param int $periods 查询最近多少期 * @return array {zones: [], matrix: [[zone_count]], color_probs: [[{red, blue, green}]]} */ public function getZoneToZoneColor($periods = 100) { $history = $this ->field('expect,num7,openTime') ->order('openTime', 'desc') ->limit($periods) ->select(); if (empty($history) || count($history) < 2) { return ['zones' => ['1-10','11-20','21-30','31-40','41-49'], 'matrix' => [], 'color_probs' => []]; } $history = array_reverse($history); $num_model = new Num(); $colorMap = $num_model->column('color', 'num'); $zoneLabels = ['1-10', '11-20', '21-30', '31-40', '41-49']; $matrix = array_fill(0, 5, array_fill(0, 5, 0)); $colorCounts = []; for ($i = 0; $i < 5; $i++) { for ($j = 0; $j < 5; $j++) { $colorCounts[$i][$j] = ['red' => 0, 'blue' => 0, 'green' => 0]; } } $getZone = function ($num) { if ($num <= 10) return 0; if ($num <= 20) return 1; if ($num <= 30) return 2; if ($num <= 40) return 3; return 4; }; for ($i = 0; $i < count($history) - 1; $i++) { $currentNum = (int)$history[$i]['num7']; $nextNum = (int)$history[$i + 1]['num7']; $from = $getZone($currentNum); $to = $getZone($nextNum); $color = $colorMap[$nextNum] ?? ''; $matrix[$from][$to]++; if (strpos($color, '红') !== false) $colorCounts[$from][$to]['red']++; elseif (strpos($color, '蓝') !== false) $colorCounts[$from][$to]['blue']++; elseif (strpos($color, '绿') !== false) $colorCounts[$from][$to]['green']++; } // 计算每个单元格波色概率 $colorProbs = array_fill(0, 5, array_fill(0, 5, ['red' => 0, 'blue' => 0, 'green' => 0])); for ($i = 0; $i < 5; $i++) { for ($j = 0; $j < 5; $j++) { $total = $matrix[$i][$j]; if ($total > 0) { $colorProbs[$i][$j] = [ 'red' => round($colorCounts[$i][$j]['red'] / $total * 100, 1), 'blue' => round($colorCounts[$i][$j]['blue'] / $total * 100, 1), 'green' => round($colorCounts[$i][$j]['green'] / $total * 100, 1), ]; } } } return [ 'zones' => $zoneLabels, 'matrix' => $matrix, 'color_probs' => $colorProbs ]; } /** * 特码热力图数据 * @param int $periods 查询最近多少期 * @return array {expects: [], heatmap: [[x, y, value]], colors: [号码对应颜色]} */ public function getSpecialHeatmap($periods = 30) { $num_model = new Num(); $colorMap = $num_model->column('color', 'num'); // 查询最近 N 期特码数据 $history = $this ->field('expect,num7,openTime') ->order('openTime', 'desc') ->limit($periods) ->select(); if (empty($history)) { return ['expects' => [], 'heatmap' => [], 'colors' => []]; } // 反转数组,使数据从左到右为从远到近 $history = array_reverse($history); $expects = []; $heatmap = []; // 构建热力图数据:[x_index, y_index, value] // x_index = 期号索引(0到periods-1) // y_index = 号码-1(0到48,号码1-49) // value = 1(出现)或 0(未出现) foreach ($history as $idx => $row) { $expects[] = (string)$row['expect']; $specialNum = (int)$row['num7']; // 标记该期特码号码出现 if ($specialNum >= 1 && $specialNum <= 49) { $heatmap[] = [$idx, $specialNum - 1, 1]; } } // 补充号码颜色映射(索引0对应号码1) $colors = []; for ($num = 1; $num <= 49; $num++) { $color = $colorMap[$num] ?? ''; if (strpos($color, '红') !== false) { $colors[] = '#e74c3c'; } elseif (strpos($color, '蓝') !== false) { $colors[] = '#3498db'; } elseif (strpos($color, '绿') !== false) { $colors[] = '#2ecc71'; } else { $colors[] = '#95a5a6'; } } return [ 'expects' => $expects, 'heatmap' => $heatmap, 'colors' => $colors, 'nums' => range(1, 49) // 号码列表 ]; } /** * 综合预测号码 * 基于历史多维度转移概率分析,给出号码预测建议 * @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. 转移概率分析 ====== // 根据历史数据量决定使用一阶或二阶马尔可夫 // 阈值条件:总期数 >= 200 且 状态对观察次数充足(>=5次的比例>=30%) $minPeriodsThreshold = 200; // 二阶马尔可夫最小历史期数阈值(从100提升到200) $minStatePairCount = 5; // 状态对最小观察次数 $use2ndOrder = false; $secondOrderAvailable = false; // 获取一阶转移概率矩阵(始终计算,作为fallback) $zoneTransition = $this->_getTransitionMatrix($allHistory, 'zone'); $tailTransition = $this->_getTransitionMatrix($allHistory, 'tail'); $headTransition = $this->_getTransitionMatrix($allHistory, 'head'); // 获取二阶转移概率矩阵(数据充足时) $zoneTransition2nd = null; $tailTransition2nd = null; $headTransition2nd = null; $prev2Zone = 0; $prev2Tail = 0; $prev2Head = 0; if (count($allHistory) >= $minPeriodsThreshold && count($allHistory) >= 2) { // 获取前两期号码属性 $prev2Special = (int)$allHistory[1]['num7']; $prev2Zone = $this->_getZoneIdx($prev2Special); $prev2Tail = $prev2Special % 10; $prev2Head = $this->_getHeadIdx($prev2Special); // 构建二阶转移矩阵 $zoneTransition2nd = $this->_getTransitionMatrix2ndOrder($allHistory, 'zone', $minStatePairCount); $tailTransition2nd = $this->_getTransitionMatrix2ndOrder($allHistory, 'tail', $minStatePairCount); $headTransition2nd = $this->_getTransitionMatrix2ndOrder($allHistory, 'head', $minStatePairCount); // 检查状态对观察次数是否充足(至少30%的状态对有足够观察) // tail类型状态空间最大(100),以tail为基准判断 if ($tailTransition2nd['total_pairs'] > 0) { $sufficientRatio = $tailTransition2nd['sufficient_pairs'] / $tailTransition2nd['total_pairs']; $secondOrderAvailable = $sufficientRatio >= 0.3; // 至少30%状态对观察>=5次 } $use2ndOrder = $secondOrderAvailable; } // 上期号码的各类属性 $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, 'transition_order' => $use2ndOrder ? 2 : 1, // 转移概率阶数 'transition_available' => $secondOrderAvailable, // 二阶是否可用 'history_count' => count($allHistory), // 历史期数 'min_periods_threshold' => $minPeriodsThreshold, // 二阶阈值 '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']; // === 转移概率得分(根据阶数选择计算方法)=== if ($use2ndOrder && $zoneTransition2nd && $tailTransition2nd && $headTransition2nd) { $transScore = $this->_calcTransitionScore2ndOrder( $num, $lastZone, $prev2Zone, $lastTail, $prev2Tail, $lastHead, $prev2Head, $zoneTransition2nd, $tailTransition2nd, $headTransition2nd, $zoneMap, $tailMap, $headMap ); $detail['trans_order'] = 2; } else { $transScore = $this->_calcTransitionScore( $num, $lastZone, $lastTail, $lastHead, $zoneTransition, $tailTransition, $headTransition, $zoneMap, $tailMap, $headMap ); $detail['trans_order'] = 1; } $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); // ====== 11. 置信度评估(新增)====== // 最小数据量阈值设为50期,不足时置信度基于估算 $minDataThreshold = 50; $confidence = $this->_calculateConfidence($predictions, $backtest, null, $minDataThreshold); // 计算命中情况 $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, 'confidence' => $confidence // 新增置信度字段 ]; } /** * 获取号码首号索引 * @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 ]; } /** * 构建二阶马尔可夫转移矩阵 * 考虑前两期状态联合决定当前转移概率 * * 状态空间说明: * - 一阶马尔可夫: N个状态 (zone:5, tail:10, head:5) * - 二阶马尔可夫: N^2个状态对 (zone:25, tail:100, head:25) * - 状态键格式: "prev1-prev2",如 "2-3" 表示前一期区域2、前两期区域3 * * 数据量阈值说明: * - 建议历史数据 >= 200期以获得稳定的二阶概率估计 * - 状态对观察次数 >= 5 才使用该状态对的二阶概率 * - 观察次数不足时返回 state_pair_insufficient 标志,供调用者回退一阶 * * @param array $history 历史数据(降序,最新在前) * @param string $type 类型:zone/tail/head * @param int $minStatePairCount 状态对最小观察次数,默认5 * @return array {matrix: [], prob_matrix: [], state_totals: [], num_categories: int, sufficient_pairs: int, total_pairs: int, min_threshold: int} */ private function _getTransitionMatrix2ndOrder($history, $type, $minStatePairCount = 5) { // 升序排列(从旧到新) $historyAsc = array_reverse($history); // 确定类别数量和索引函数 switch ($type) { case 'zone': $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': $numCategories = 10; $getIdx = function ($num) { return $num % 10; }; break; case 'head': $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; default: return [ 'matrix' => [], 'prob_matrix' => [], 'state_totals' => [], 'num_categories' => 0, 'sufficient_pairs' => 0, 'total_pairs' => 0, 'min_threshold' => $minStatePairCount ]; } // 状态空间: (prev1, prev2) -> current,共 numCategories^2 个前置状态 $matrix = []; $stateTotals = []; // 初始化矩阵结构 for ($i = 0; $i < $numCategories; $i++) { for ($j = 0; $j < $numCategories; $j++) { $stateKey = $i . '-' . $j; $matrix[$stateKey] = array_fill(0, $numCategories, 0); $stateTotals[$stateKey] = 0; } } // 统计二阶转移 for ($i = 0; $i < count($historyAsc) - 2; $i++) { $prev1 = $getIdx((int)$historyAsc[$i]['num7']); $prev2 = $getIdx((int)$historyAsc[$i + 1]['num7']); $current = $getIdx((int)$historyAsc[$i + 2]['num7']); if ($prev1 < 0 || $prev2 < 0 || $current < 0) continue; $stateKey = $prev1 . '-' . $prev2; $matrix[$stateKey][$current]++; $stateTotals[$stateKey]++; } // 统计充分观察的状态对数量(观察次数 >= minStatePairCount) $sufficientPairs = 0; $totalPairs = $numCategories * $numCategories; foreach ($stateTotals as $stateKey => $count) { if ($count >= $minStatePairCount) { $sufficientPairs++; } } // 拉普拉斯平滑处理 $probMatrix = []; foreach ($matrix as $stateKey => $counts) { $smoothTotal = $stateTotals[$stateKey] + $numCategories; $probMatrix[$stateKey] = []; for ($j = 0; $j < $numCategories; $j++) { $probMatrix[$stateKey][$j] = ($counts[$j] + 1) / $smoothTotal; } } return [ 'matrix' => $matrix, 'prob_matrix' => $probMatrix, 'state_totals' => $stateTotals, 'num_categories' => $numCategories, 'sufficient_pairs' => $sufficientPairs, 'total_pairs' => $totalPairs, 'min_threshold' => $minStatePairCount ]; } /** * 分析单双规律 * @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); } /** * 计算二阶转移概率得分 * * 计算方法: * - 综合区域、尾号、首号三个维度的二阶转移概率 * - 各维度权重: 区域40%、尾号35%、首号25% * - 得分范围: 0-100 * * @param int $num 当前号码 * @param int $prev1Zone 前一期区域索引 * @param int $prev2Zone 前两期区域索引 * @param int $prev1Tail 前一期尾号索引 * @param int $prev2Tail 前两期尾号索引 * @param int $prev1Head 前一期首号索引 * @param int $prev2Head 前两期首号索引 * @param array $zoneTrans2nd 二阶区域转移矩阵 * @param array $tailTrans2nd 二阶尾号转移矩阵 * @param array $headTrans2nd 二阶首号转移矩阵 * @param array $zoneMap 号码区域映射 * @param array $tailMap 号码尾号映射 * @param array $headMap 号码首号映射 * @return float 综合转移得分 (0-100) */ private function _calcTransitionScore2ndOrder( $num, $prev1Zone, $prev2Zone, $prev1Tail, $prev2Tail, $prev1Head, $prev2Head, $zoneTrans2nd, $tailTrans2nd, $headTrans2nd, $zoneMap, $tailMap, $headMap ) { $zone = $zoneMap[$num]; $tail = $tailMap[$num]; $head = $headMap[$num]; $score = 0; // 区域二阶转移得分(权重40%) $zoneStateKey = $prev1Zone . '-' . $prev2Zone; if (isset($zoneTrans2nd['prob_matrix'][$zoneStateKey][$zone])) { $prob = $zoneTrans2nd['prob_matrix'][$zoneStateKey][$zone]; $score += $prob * 40; } // 尾号二阶转移得分(权重35%) $tailStateKey = $prev1Tail . '-' . $prev2Tail; if (isset($tailTrans2nd['prob_matrix'][$tailStateKey][$tail])) { $prob = $tailTrans2nd['prob_matrix'][$tailStateKey][$tail]; $score += $prob * 35; } // 首号二阶转移得分(权重25%) $headStateKey = $prev1Head . '-' . $prev2Head; if (isset($headTrans2nd['prob_matrix'][$headStateKey][$head])) { $prob = $headTrans2nd['prob_matrix'][$headStateKey][$head]; $score += $prob * 25; } return round($score, 2); } /** * 计算单双平衡得分 * @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; } /** * 计算预测置信度 * * 置信度组成(三个维度加权平均): * - 维度1: 历史排名命中率 (权重0.4) - 基于回测数据统计各排名位置的命中率 * - 维度2: 得分分布置信度 (权重0.3) - 当前号码得分与Top5得分范围的比例关系 * - 维度3: 得分集中度 (权重0.3) - Top5得分与平均得分的差距,差距越大置信度越高 * * 加权公式: * confidence = 0.4 * historical_hit_rate + 0.3 * score_distribution + 0.3 * score_concentration * * 阈值定义: * - 高置信度: >= 70% (绿色展示) * - 中置信度: 50-70% (橙色展示) * - 低置信度: < 50% (红色展示) * * @param array $predictions 预测结果数组(Top5) * @param array $backtest 回测结果 * @param array $scoresAll 所有号码得分详情(可选,用于集中度计算) * @param int $minDataThreshold 最小数据量阈值,默认50期 * @return array {confidence_scores: [], overall_confidence: float, data_warning: string|null} */ private function _calculateConfidence($predictions, $backtest, $scoresAll = null, $minDataThreshold = 50) { // 数据量检查 $dataWarning = null; $hasBacktest = $backtest && !empty($backtest['details']) && $backtest['total_tests'] > 0; if (!$hasBacktest || $backtest['total_tests'] < $minDataThreshold) { $dataWarning = '回测数据不足(' . ($backtest['total_tests'] ?? 0) . '期),置信度基于估算,建议至少50期'; } $confidenceScores = []; // 计算Top5平均得分(用于集中度计算) $avgScore = 0; if (!empty($predictions)) { $totalScore = array_sum(array_column($predictions, 'score')); $avgScore = $totalScore / count($predictions); } foreach ($predictions as $idx => $pred) { $rank = $idx + 1; $num = $pred['num']; $score = $pred['score']; // 维度1: 历史排名命中率 (权重0.4) $rankHitRate = $this->_getHistoricalHitRateByRank($rank, $backtest); // 维度2: 得分分布置信度 (权重0.3) - 得分比例 $scoreDistribution = $this->_getScoreDistributionConfidence($score, $predictions); // 维度3: 得分集中度 (权重0.3) - Top得分与平均得分的差距比例 $scoreConcentration = $this->_getScoreConcentration($score, $avgScore, $predictions); // 综合置信度(加权平均) $overallConfidence = $rankHitRate * 0.4 + $scoreDistribution * 0.3 + $scoreConcentration * 0.3; $confidenceScores[] = [ 'num' => $num, 'rank' => $rank, 'confidence' => round($overallConfidence * 100, 1), 'rank_hit_rate' => round($rankHitRate * 100, 1), 'score_distribution' => round($scoreDistribution * 100, 1), 'score_concentration' => round($scoreConcentration * 100, 1) ]; } // 整体置信度(Top5平均) $overallConfidence = count($confidenceScores) > 0 ? round(array_sum(array_column($confidenceScores, 'confidence')) / count($confidenceScores), 1) : 0; return [ 'confidence_scores' => $confidenceScores, 'overall_confidence' => $overallConfidence, 'data_warning' => $dataWarning ]; } /** * 基于历史排名获取命中率 * * 计算方法: * - 有回测数据时: 统计各排名的历史命中次数 / 总测试次数 * - 无回测数据时: 根据排名估算,排名越靠前置信度越高 * 估算公式: 1 - (rank - 1) * 0.15,即第1名估算85%,第5名估算25% * * @param int $rank 排名位置 (1-5) * @param array $backtest 回测结果 * @return float 该排名的历史命中率 (0-1) */ private function _getHistoricalHitRateByRank($rank, $backtest) { if (!$backtest || empty($backtest['details']) || $backtest['total_tests'] == 0) { // 无回测数据时,根据排名估算(排名越靠前置信度越高) // 估算公式: 1 - (rank - 1) * 0.15 // 第1名: 1.0, 第2名: 0.85, 第3名: 0.70, 第4名: 0.55, 第5名: 0.40 return max(0, 1 - ($rank - 1) * 0.15); } // 统计各排名的历史命中次数 $rankHits = array_fill(1, 5, 0); foreach ($backtest['details'] as $detail) { if ($detail['hit'] && $detail['rank'] >= 1 && $detail['rank'] <= 5) { $rankHits[$detail['rank']]++; } } $totalTests = $backtest['total_tests']; return $totalTests > 0 ? $rankHits[$rank] / $totalTests : 0; } /** * 计算得分分布置信度 * * 计算方法: * - 得分比例 = (score - bottomScore) / (topScore - bottomScore) * - 得分越接近第一名,置信度越高 * - 所有得分相同时返回1 * * @param float $score 当前号码得分 * @param array $predictions 所有预测结果 * @return float 得分置信度 (0-1) */ private function _getScoreDistributionConfidence($score, $predictions) { if (empty($predictions)) return 0; $topScore = $predictions[0]['score']; $bottomScore = end($predictions)['score']; if ($topScore == $bottomScore) return 1; // 所有得分相同 // 得分比例:(score - bottom) / (top - bottom) $ratio = ($score - $bottomScore) / ($topScore - $bottomScore); return max(0, min(1, $ratio)); } /** * 计算得分集中度 * * 计算方法: * - 集中度 = (score - avgScore) / (topScore - avgScore) 如果 score > avgScore * - 集中度 = 0 如果 score <= avgScore * - Top得分与平均得分差距越大,集中度越高,表示预测结果区分度明显 * * @param float $score 当前号码得分 * @param float $avgScore Top5平均得分 * @param array $predictions 所有预测结果 * @return float 集中度置信度 (0-1) */ private function _getScoreConcentration($score, $avgScore, $predictions) { if (empty($predictions)) return 0; $topScore = $predictions[0]['score']; // 如果得分低于平均,集中度为0 if ($score <= $avgScore) { return 0; } // 如果Top得分等于平均,所有得分相同,集中度为0.5 if ($topScore == $avgScore) { return $score == $topScore ? 0.5 : 0; } // 集中度 = (score - avg) / (top - avg) $concentration = ($score - $avgScore) / ($topScore - $avgScore); return max(0, min(1, $concentration)); } /** * 权重网格搜索优化 * * 优化目标定义: * - 综合评估得分 = hit_rate * 0.6 + ndcg_5 * 100 * 0.4 * - 命中率权重60%,NDCG权重40% * - 返回综合得分最高的权重配置 * * 5种预定义权重配置: * - 配置1: 遗漏优先型 - omit_regression权重最高(0.25) * - 配置2: 转移概率优先型 - transition_prob权重最高(0.25) * - 配置3: 走势方向优先型 - trend_direction权重最高(0.25) * - 配置4: 平衡型 - 各维度权重较均衡 * - 配置5: 组合特征优先型 - combination权重最高(0.20) * * @param int $periods 统计期数,范围50-500 * @param int $backtestCount 回测期数,范围10-100 * @param int $timeoutSeconds 超时限制秒数,默认60秒 * @return array {best_weights: [], best_hit_rate: float, best_ndcg: float, all_results: [], timed_out: bool} */ private function _optimizeWeightsGridSearch($periods = 200, $backtestCount = 50, $timeoutSeconds = 60) { // 超时保护:记录开始时间 $startTime = microtime(true); $timedOut = false; // 5种预定义权重配置(具体权重值明确) $weightConfigs = [ // 配置1: 遗漏优先型 - 遗漏回归权重最高 [ 'omit_regression' => 0.25, // 遗漏回归权重25% 'freq_regression' => 0.12, // 频率回归权重12% 'transition_prob' => 0.15, // 转移概率权重15% 'trend_direction' => 0.12, // 走势方向权重12% 'oddeven_balance' => 0.08, // 单双平衡权重8% 'bigsmall_balance' => 0.08, // 大小平衡权重8% 'zone_balance' => 0.05, // 区域平衡权重5% 'color_balance' => 0.05, // 波色平衡权重5% 'combination' => 0.10 // 组合特征权重10% ], // 配置2: 转移概率优先型 - 转移概率权重最高 [ 'omit_regression' => 0.15, 'freq_regression' => 0.10, 'transition_prob' => 0.25, // 转移概率权重25%(最高) 'trend_direction' => 0.12, 'oddeven_balance' => 0.08, 'bigsmall_balance' => 0.08, 'zone_balance' => 0.04, 'color_balance' => 0.04, 'combination' => 0.14 ], // 配置3: 走势方向优先型 - 走势方向权重最高 [ 'omit_regression' => 0.12, 'freq_regression' => 0.10, 'transition_prob' => 0.15, 'trend_direction' => 0.25, // 走势方向权重25%(最高) 'oddeven_balance' => 0.08, 'bigsmall_balance' => 0.08, 'zone_balance' => 0.04, 'color_balance' => 0.04, 'combination' => 0.12 ], // 配置4: 平衡型(默认配置)- 各维度权重较均衡 [ 'omit_regression' => 0.18, 'freq_regression' => 0.12, 'transition_prob' => 0.18, 'trend_direction' => 0.14, 'oddeven_balance' => 0.08, 'bigsmall_balance' => 0.08, 'zone_balance' => 0.04, 'color_balance' => 0.04, 'combination' => 0.10 ], // 配置5: 组合特征优先型 - 组合特征权重最高 [ 'omit_regression' => 0.15, 'freq_regression' => 0.10, 'transition_prob' => 0.15, 'trend_direction' => 0.12, 'oddeven_balance' => 0.06, 'bigsmall_balance' => 0.06, 'zone_balance' => 0.03, 'color_balance' => 0.03, 'combination' => 0.20 // 组合特征权重20%(最高) ] ]; $bestWeights = []; $bestHitRate = 0; $bestNdcg = 0; $bestCombinedScore = 0; $allResults = []; // 执行每种配置的回测(添加超时检查) foreach ($weightConfigs as $configIdx => $weights) { // 超时检查:超过限制时间则停止 $elapsedTime = microtime(true) - $startTime; if ($elapsedTime > $timeoutSeconds) { $timedOut = true; break; } // 执行回测 $backtest = $this->_runBacktestV3($periods, $weights, $backtestCount); $hitRate = $backtest['hit_rate'] ?? 0; $ndcg = $backtest['ndcg_5'] ?? 0; $avgRank = $backtest['avg_rank'] ?? 0; $mrr = $backtest['mrr'] ?? 0; // 综合评估得分:命中率60% + NDCG40% $combinedScore = $hitRate * 0.6 + $ndcg * 100 * 0.4; $result = [ 'config_name' => $configIdx + 1, 'config_type' => ['遗漏优先型', '转移概率优先型', '走势方向优先型', '平衡型', '组合特征优先型'][$configIdx], 'weights' => $weights, 'hit_rate' => $hitRate, 'avg_rank' => $avgRank, 'ndcg_5' => $ndcg, 'mrr' => $mrr, 'combined_score' => round($combinedScore, 2), 'total_hits' => $backtest['total_hits'] ?? 0 ]; $allResults[] = $result; // 更新最优配置 if ($combinedScore > $bestCombinedScore) { $bestCombinedScore = $combinedScore; $bestHitRate = $hitRate; $bestNdcg = $ndcg; $bestWeights = $weights; } } // 按综合得分降序排序结果 usort($allResults, function($a, $b) { return $b['combined_score'] - $a['combined_score']; }); return [ 'best_weights' => $bestWeights, 'best_hit_rate' => $bestHitRate, 'best_ndcg' => $bestNdcg, 'best_combined_score' => round($bestCombinedScore, 2), 'all_results' => $allResults, 'periods' => $periods, 'backtest_count' => $backtestCount, 'timeout_seconds' => $timeoutSeconds, 'timed_out' => $timedOut, 'elapsed_time' => round(microtime(true) - $startTime, 2) ]; } /** * 基于正码关联规律的特码预测方法(修正版) * 核心规律:上期正码 → 当期特码 * - 覆盖区间规律:91.44% 当期特码在上期正码覆盖的区间内 * - 正码±3距离:59.36% 当期特码与上期正码某号码距离≤3 * - 双波色预测:69.52% 当期特码波色在上期正码前2种主导波色内 * - 特码区间转移:77.54% 基于上期特码区间预测当期特码区间 * - 平均值±10:41.98% 当期特码在上期正码平均值±10范围 * - 尾数±2:50% 和值尾数与特码尾数差≤2 * @param int $periods 统计期数(用于验证历史命中率) * @param string $targetExpect 目标期号(可选,用于回测验证) * @return array {predictions: [], analysis: {}, hit_info: {}, backtest: {}} */ public function getPredictionByNormalRelation($periods = 100, $targetExpect = '') { $num_model = new Num(); $colorMap = $num_model->column('color', 'num'); $animalMap = $num_model->column('animal', 'num'); // 区间划分:大号(31-49)、中号(11-30)、小号(1-10) $getBigZone = function ($num) { if ($num <= 10) return 'small'; if ($num <= 30) return 'mid'; return 'big'; }; // 细区间划分:1-10, 11-20, 21-30, 31-40, 41-49 $getFineZone = function ($num) { if ($num <= 10) return 0; if ($num <= 20) return 1; if ($num <= 30) return 2; if ($num <= 40) return 3; return 4; }; // 确定预测基准 $actualResult = null; $lastNormals = []; $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']] ?? '', 'bigZone' => $getBigZone($targetRow['num7']), 'openTime' => $targetRow['openTime'] ]; // 获取上一期数据作为预测基准 $prevRow = $this->where('openTime', '<', $cutoffTime)->order('openTime', 'desc')->limit(1)->find(); if (!$prevRow) { return ['predictions' => [], 'error' => '没有历史数据']; } for ($i = 1; $i <= 6; $i++) { $lastNormals[] = (int)$prevRow['num' . $i]; } $lastSpecial = (int)$prevRow['num7']; $lastExpect = (string)$prevRow['expect']; } else { // 使用最新一期作为预测基准 $latest = $this->field('expect,num1,num2,num3,num4,num5,num6,num7,openTime') ->order('openTime', 'desc')->limit(1)->find(); if (!$latest) { return ['predictions' => [], 'error' => '没有历史数据']; } for ($i = 1; $i <= 6; $i++) { $lastNormals[] = (int)$latest['num' . $i]; } $lastSpecial = (int)$latest['num7']; $lastExpect = (string)$latest['expect']; } // 分析上期正码特征 $normalMin = min($lastNormals); $normalMax = max($lastNormals); $normalAvg = round(array_sum($lastNormals) / 6, 2); $normalSum = array_sum($lastNormals); // 统计上期正码波色分布(找出前2种主导波色) $colorCounts = ['红' => 0, '蓝' => 0, '绿' => 0]; foreach ($lastNormals as $n) { $color = $colorMap[$n] ?? ''; if (strpos($color, '红') !== false) $colorCounts['红']++; elseif (strpos($color, '蓝') !== false) $colorCounts['蓝']++; elseif (strpos($color, '绿') !== false) $colorCounts['绿']++; } // 按数量排序,取前2种 $sortedColors = []; foreach ($colorCounts as $c => $cnt) { $sortedColors[] = ['color' => $c, 'count' => $cnt]; } usort($sortedColors, function ($a, $b) { return $b['count'] - $a['count']; }); $top2Colors = [$sortedColors[0]['color'], $sortedColors[1]['color']]; // 获取上期正码覆盖的细区间 $normalFineZones = []; foreach ($lastNormals as $n) { $zoneIdx = $getFineZone($n); if (!in_array($zoneIdx, $normalFineZones)) { $normalFineZones[] = $zoneIdx; } } // 获取上期正码覆盖的大区间 $normalBigZones = []; foreach ($lastNormals as $n) { $bigZone = $getBigZone($n); if (!in_array($bigZone, $normalBigZones)) { $normalBigZones[] = $bigZone; } } // 上期特码所在大区间 $lastSpecialBigZone = $getBigZone($lastSpecial); // 特码区间转移概率矩阵(基于历史分析) $zoneTransMatrix = [ 'big' => ['big' => 35.77, 'mid' => 37.96, 'small' => 26.28], 'mid' => ['big' => 33.77, 'mid' => 43.51, 'small' => 22.73], 'small' => ['big' => 42.17, 'mid' => 42.17, 'small' => 15.66] ]; // 计算每个号码的预测评分 $predictions = []; for ($num = 1; $num <= 49; $num++) { $numColor = $colorMap[$num] ?? ''; $numBigZone = $getBigZone($num); $numFineZone = $getFineZone($num); // 规律1:覆盖区间规律(91.44%命中)- 细区间覆盖 $fineZoneCovered = in_array($numFineZone, $normalFineZones); $zoneCoverScore = $fineZoneCovered ? 91 : 0; // 规律2:正码±3距离(59.36%命中) $minDistance = 49; foreach ($lastNormals as $n) { $dist = abs($num - $n); if ($dist < $minDistance) $minDistance = $dist; } $distScore = $minDistance <= 3 ? 59 : ($minDistance <= 5 ? 40 : 0); // 规律3:双波色预测(69.52%命中) $colorInTop2 = false; foreach ($top2Colors as $tc) { if (strpos($numColor, $tc) !== false) { $colorInTop2 = true; break; } } $colorScore = $colorInTop2 ? 69 : 0; // 规律4:特码区间转移(77.54%命中) $transProb = $zoneTransMatrix[$lastSpecialBigZone][$numBigZone] ?? 0; // 取该区间最高转移概率的2个区间 $transProbs = $zoneTransMatrix[$lastSpecialBigZone]; $sortedTrans = []; foreach ($transProbs as $z => $p) { $sortedTrans[] = ['zone' => $z, 'prob' => $p]; } usort($sortedTrans, function ($a, $b) { return $b['prob'] - $a['prob']; }); $top2TransZones = [$sortedTrans[0]['zone'], $sortedTrans[1]['zone']]; $transMatch = in_array($numBigZone, $top2TransZones); $transScore = $transMatch ? 77 : 0; // 规律5:平均值±10(41.98%命中) $avgDiff = abs($num - $normalAvg); $avgScore = $avgDiff <= 10 ? 42 : 0; // 规律6:尾数±2(50%命中) $numTail = $num % 10; $sumTail = $normalSum % 10; $tailDiff = abs($numTail - $sumTail); $tailDiff = min($tailDiff, 10 - $tailDiff); $tailScore = $tailDiff <= 2 ? 50 : ($tailDiff <= 3 ? 30 : 0); // 综合评分(加权求和) $totalScore = $zoneCoverScore * 0.30 // 覆盖区间权重最高 + $transScore * 0.25 // 特码区间转移 + $colorScore * 0.20 // 双波色 + $distScore * 0.12 // 距离 + $avgScore * 0.08 // 平均值 + $tailScore * 0.05; // 尾数 $predictions[] = [ 'num' => $num, 'score' => round($totalScore, 2), 'color' => $numColor, 'animal' => $animalMap[$num] ?? '', 'big_zone' => $numBigZone, 'fine_zone' => $numFineZone, 'zone_covered' => $fineZoneCovered, 'min_distance' => $minDistance, 'color_in_top2' => $colorInTop2, 'trans_match' => $transMatch, 'trans_prob' => $transProb, 'avg_diff' => round($avgDiff, 2), 'tail_diff' => $tailDiff ]; } // 按评分降序排序 usort($predictions, function ($a, $b) { return $b['score'] - $a['score']; }); // 返回Top15推荐号码 $topPredictions = array_slice($predictions, 0, 15); // 分析信息 $analysis = [ 'last_expect' => $lastExpect, 'last_normals' => $lastNormals, 'last_special' => $lastSpecial, 'last_special_zone' => $lastSpecialBigZone, 'normal_min' => $normalMin, 'normal_max' => $normalMax, 'normal_avg' => $normalAvg, 'normal_sum' => $normalSum, 'top2_colors' => $top2Colors, 'color_counts' => $colorCounts, 'normal_fine_zones' => $normalFineZones, 'normal_big_zones' => $normalBigZones, 'zone_trans_matrix' => $zoneTransMatrix, 'rules' => [ ['name' => '覆盖区间', 'rate' => '91.44%', 'desc' => '当期特码在上期正码覆盖的细区间内'], ['name' => '特码区间转移', 'rate' => '77.54%', 'desc' => '基于上期特码区间预测当期特码所在大区间'], ['name' => '双波色预测', 'rate' => '69.52%', 'desc' => '当期特码波色在上期正码前2种主导波色内'], ['name' => '正码±3距离', 'rate' => '59.36%', 'desc' => '当期特码与上期正码某号码距离≤3'], ['name' => '尾数±2', 'rate' => '50%', 'desc' => '上期正码和值尾数与当期特码尾数差≤2'], ['name' => '平均值±10', 'rate' => '41.98%', 'desc' => '当期特码在上期正码平均值±10范围'] ], 'predict_next_expect' => $lastExpect ? (string)(intval($lastExpect) + 1) : '' ]; // 命中验证 $hitInfo = null; if ($actualResult) { $hitRank = -1; foreach ($topPredictions as $idx => $p) { if ($p['num'] === $actualResult['num7']) { $hitRank = $idx + 1; break; } } $fullRank = -1; foreach ($predictions as $idx => $p) { if ($p['num'] === $actualResult['num7']) { $fullRank = $idx + 1; break; } } // 分析实际结果的规律命中情况 $actualAnalysis = null; foreach ($predictions as $p) { if ($p['num'] === $actualResult['num7']) { $actualAnalysis = $p; break; } } $hitInfo = [ 'hit' => $hitRank > 0, 'rank_in_top' => $hitRank, 'rank_in_all' => $fullRank, 'actual_num' => $actualResult['num7'], 'actual_color' => $actualResult['color'], 'actual_animal' => $actualResult['animal'], 'actual_expect' => $actualResult['expect'], 'actual_zone_covered' => $actualAnalysis ? $actualAnalysis['zone_covered'] : false, 'actual_min_distance' => $actualAnalysis ? $actualAnalysis['min_distance'] : 99, 'actual_color_in_top2' => $actualAnalysis ? $actualAnalysis['color_in_top2'] : false, 'actual_trans_match' => $actualAnalysis ? $actualAnalysis['trans_match'] : false, 'actual_tail_diff' => $actualAnalysis ? $actualAnalysis['tail_diff'] : 99, 'actual_avg_diff' => $actualAnalysis ? $actualAnalysis['avg_diff'] : 99 ]; } // 回测验证(默认显示前50期命中详情) $backtest = null; if ($periods >= 30) { $backtest = $this->_runBacktestNormalRelation($periods, 50); } return [ 'predictions' => $topPredictions, 'all_predictions' => $predictions, 'analysis' => $analysis, 'actual_result' => $actualResult, 'hit_info' => $hitInfo, 'backtest' => $backtest ]; } /** * 执行正码关联规律的历史回测(修正版) * 使用正确规律:上期正码 → 当期特码 * @param int $periods 回测期数 * @param int $detailLimit 返回详情条数 * @return array {hit_rate, avg_rank, details, rule_stats} */ private function _runBacktestNormalRelation($periods = 100, $detailLimit = 20) { $history = $this->field('expect,num1,num2,num3,num4,num5,num6,num7,openTime') ->order('openTime', 'desc') ->limit($periods + 1) ->select(); if (count($history) < 2) { return ['error' => '数据不足']; } $num_model = new Num(); $colorMap = $num_model->column('color', 'num'); // 大区间划分 $getBigZone = function ($num) { if ($num <= 10) return 'small'; if ($num <= 30) return 'mid'; return 'big'; }; // 细区间划分 $getFineZone = function ($num) { if ($num <= 10) return 0; if ($num <= 20) return 1; if ($num <= 30) return 2; if ($num <= 40) return 3; return 4; }; // 特码区间转移概率矩阵 $zoneTransMatrix = [ 'big' => ['big' => 35.77, 'mid' => 37.96, 'small' => 26.28], 'mid' => ['big' => 33.77, 'mid' => 43.51, 'small' => 22.73], 'small' => ['big' => 42.17, 'mid' => 42.17, 'small' => 15.66] ]; $hits = 0; $ranks = []; $details = []; $ruleHits = [ 'zone_cover' => 0, 'trans_match' => 0, 'color_top2' => 0, 'dist_3' => 0, 'tail_2' => 0, 'avg_10' => 0 ]; for ($i = 0; $i < count($history) - 1; $i++) { $currentRow = $history[$i]; $prevRow = $history[$i + 1]; // 使用上一期的正码预测当期的特码 $lastNormals = []; for ($j = 1; $j <= 6; $j++) { $lastNormals[] = (int)$prevRow['num' . $j]; } $lastSpecial = (int)$prevRow['num7']; $actualSpecial = (int)$currentRow['num7']; // 分析上期正码特征 $normalMin = min($lastNormals); $normalMax = max($lastNormals); $normalAvg = array_sum($lastNormals) / 6; $normalSum = array_sum($lastNormals); // 波色分布 $colorCounts = ['红' => 0, '蓝' => 0, '绿' => 0]; foreach ($lastNormals as $n) { $color = $colorMap[$n] ?? ''; if (strpos($color, '红') !== false) $colorCounts['红']++; elseif (strpos($color, '蓝') !== false) $colorCounts['蓝']++; elseif (strpos($color, '绿') !== false) $colorCounts['绿']++; } $sortedColors = []; foreach ($colorCounts as $c => $cnt) { $sortedColors[] = ['color' => $c, 'count' => $cnt]; } usort($sortedColors, function ($a, $b) { return $b['count'] - $a['count']; }); $top2Colors = [$sortedColors[0]['color'], $sortedColors[1]['color']]; // 上期正码覆盖的细区间 $normalFineZones = []; foreach ($lastNormals as $n) { $zoneIdx = $getFineZone($n); if (!in_array($zoneIdx, $normalFineZones)) { $normalFineZones[] = $zoneIdx; } } // 上期特码所在大区间 $lastSpecialBigZone = $getBigZone($lastSpecial); // 计算每个号码评分 $scores = []; for ($num = 1; $num <= 49; $num++) { $numColor = $colorMap[$num] ?? ''; $numBigZone = $getBigZone($num); $numFineZone = $getFineZone($num); // 规律1:覆盖区间(91%) $zoneCovered = in_array($numFineZone, $normalFineZones); $zoneCoverScore = $zoneCovered ? 91 : 0; // 规律2:特码区间转移(77%) $transProbs = $zoneTransMatrix[$lastSpecialBigZone]; $sortedTrans = []; foreach ($transProbs as $z => $p) { $sortedTrans[] = ['zone' => $z, 'prob' => $p]; } usort($sortedTrans, function ($a, $b) { return $b['prob'] - $a['prob']; }); $top2TransZones = [$sortedTrans[0]['zone'], $sortedTrans[1]['zone']]; $transMatch = in_array($numBigZone, $top2TransZones); $transScore = $transMatch ? 77 : 0; // 规律3:双波色(69%) $colorInTop2 = false; foreach ($top2Colors as $tc) { if (strpos($numColor, $tc) !== false) { $colorInTop2 = true; break; } } $colorScore = $colorInTop2 ? 69 : 0; // 规律4:距离≤3(59%) $minDist = 49; foreach ($lastNormals as $n) { $dist = abs($num - $n); if ($dist < $minDist) $minDist = $dist; } $distScore = $minDist <= 3 ? 59 : ($minDist <= 5 ? 40 : 0); // 规律5:尾数≤2(50%) $tailDiff = abs($num % 10 - $normalSum % 10); $tailDiff = min($tailDiff, 10 - $tailDiff); $tailScore = $tailDiff <= 2 ? 50 : ($tailDiff <= 3 ? 30 : 0); // 规律6:平均值±10(42%) $avgDiff = abs($num - $normalAvg); $avgScore = $avgDiff <= 10 ? 42 : 0; // 综合评分 $score = $zoneCoverScore * 0.30 + $transScore * 0.25 + $colorScore * 0.20 + $distScore * 0.12 + $avgScore * 0.08 + $tailScore * 0.05; $scores[$num] = $score; } // 排序找排名 $sorted = []; for ($num = 1; $num <= 49; $num++) { $sorted[] = ['num' => $num, 'score' => $scores[$num]]; } usort($sorted, function ($a, $b) { return $b['score'] - $a['score']; }); $rank = -1; foreach ($sorted as $idx => $item) { if ($item['num'] === $actualSpecial) { $rank = $idx + 1; break; } } if ($rank > 0 && $rank <= 15) { $hits++; } $ranks[] = $rank; // 统计各规律命中情况 $actualFineZone = $getFineZone($actualSpecial); $actualBigZone = $getBigZone($actualSpecial); $actualColor = $colorMap[$actualSpecial] ?? ''; $actualMinDist = 49; foreach ($lastNormals as $n) { $dist = abs($actualSpecial - $n); if ($dist < $actualMinDist) $actualMinDist = $dist; } $actualTailDiff = abs($actualSpecial % 10 - $normalSum % 10); $actualTailDiff = min($actualTailDiff, 10 - $actualTailDiff); $actualAvgDiff = abs($actualSpecial - $normalAvg); if (in_array($actualFineZone, $normalFineZones)) $ruleHits['zone_cover']++; if (in_array($actualBigZone, $top2TransZones)) $ruleHits['trans_match']++; $actualColorInTop2 = false; foreach ($top2Colors as $tc) { if (strpos($actualColor, $tc) !== false) $actualColorInTop2 = true; } if ($actualColorInTop2) $ruleHits['color_top2']++; if ($actualMinDist <= 3) $ruleHits['dist_3']++; if ($actualTailDiff <= 2) $ruleHits['tail_2']++; if ($actualAvgDiff <= 10) $ruleHits['avg_10']++; $details[] = [ 'expect' => (string)$currentRow['expect'], 'actual' => $actualSpecial, 'rank' => $rank, 'hit' => $rank > 0 && $rank <= 15 ]; } $totalPeriods = count($ranks); $hitRate = $totalPeriods > 0 ? round($hits / $totalPeriods * 100, 2) : 0; $avgRank = $totalPeriods > 0 ? round(array_sum($ranks) / $totalPeriods, 2) : 0; // 计算各规律实际命中率 $ruleStats = []; foreach ($ruleHits as $rule => $count) { $ruleStats[$rule] = [ 'hits' => $count, 'rate' => $totalPeriods > 0 ? round($count / $totalPeriods * 100, 2) : 0 ]; } return [ 'periods' => $totalPeriods, 'hits' => $hits, 'hit_rate' => $hitRate, 'avg_rank' => $avgRank, 'rule_stats' => $ruleStats, 'details' => array_slice($details, 0, $detailLimit) ]; } // ============================================================ // V4 预测算法:贝叶斯对数似然集成 + 指数时间衰减 + 模式匹配 // ============================================================ /** * V4 智能预测算法 — 贝叶斯集成学习 * * 核心改进(相对 V3): * 1. 指数时间衰减:近期数据获得更高权重 (half-life = 50 期) * 2. 贝叶斯对数似然融合:替代线性加权求和,各维度在 log-space 独立更新后验概率 * 3. 号码级马尔可夫转移:直接建模 P(num_t | num_{t-1}),而非仅属性转移 * 4. 历史形态匹配:在特征空间找最相似的历史片段,用其后验分布预测 * 5. 周期性自相关检测:ACF 探测号码的隐藏周期规律 * 6. Softmax 概率输出:分数转化为校准后的真实概率分布 * 7. 自适应集成权重:基于回测滚动性能动态调整子模型权重 * * @param int $periods 历史期数 (30-500) * @param array $weights 子模型权重(可选,覆盖自适应权重) * @param string $targetExpect 验证目标期号(可选) * @param bool $skipBacktest 跳过回测 * @param int $backtestCount 回测期数 * @return array */ public function getPredictionV4($periods = 200, $weights = [], $targetExpect = '', $skipBacktest = false, $backtestCount = 50) { set_time_limit(120); // V4 计算量较大 $num_model = new Num(); $colorMap = $num_model->column('color', 'num'); $animalMap = $num_model->column('animal', 'num'); // —— 子模型默认权重(初始值,会被自适应调整覆盖)—— $defaultWeights = [ 'freq_ewma' => 0.20, // 时间衰减频率 'markov_number' => 0.22, // 号码级马尔可夫转移 'omit_empirical' => 0.17, // 经验 CDF 遗漏回归 'pattern_match' => 0.18, // 历史形态匹配 'cyclical' => 0.09, // 周期性检测 'attr_balance' => 0.14, // 属性平衡(奇偶/大小/区域/波色) ]; $weights = $weights ? array_merge($defaultWeights, $weights) : $defaultWeights; // —— 获取历史数据 —— $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' => '目标期号之前没有历史数据']; } $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); $totalDraws = count($allHistory); // —— 指数时间衰减权重 —— $halfLife = 50; // 半衰期:50期前的数据权重为最近的 1/2 $lambda = log(2) / $halfLife; $timeWeights = []; for ($t = 0; $t < $totalDraws; $t++) { // t=0 是最旧, t=totalDraws-1 是最新 $age = $totalDraws - 1 - $t; $timeWeights[$t] = exp(-$lambda * $age); } $twSum = array_sum($timeWeights); if ($twSum > 0) { foreach ($timeWeights as $k => $v) { $timeWeights[$k] = $v / $twSum * $totalDraws; } } // —— 预计算号码属性 —— $zoneMap = []; $tailMap = []; $headMap = []; $isOddMap = []; $isBigMap = []; $colorKeyMap = []; 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); $c = $colorMap[$n] ?? ''; if (strpos($c, '红') !== false) $colorKeyMap[$n] = '红'; elseif (strpos($c, '蓝') !== false) $colorKeyMap[$n] = '蓝'; elseif (strpos($c, '绿') !== false) $colorKeyMap[$n] = '绿'; else $colorKeyMap[$n] = ''; } // ============================================================ // 子模型 1: 时间衰减频率 (EWMA Frequency) // ============================================================ $ewmaFreq = array_fill(1, 49, 0.0); foreach ($historyAsc as $idx => $row) { $num = (int)$row['num7']; if ($num >= 1 && $num <= 49) { $ewmaFreq[$num] += $timeWeights[$idx]; } } // 归一化为概率 $ewmaSum = array_sum($ewmaFreq); $ewmaLogProb = []; for ($n = 1; $n <= 49; $n++) { $p = max($ewmaFreq[$n] / max($ewmaSum, 0.001), 0.0001); $ewmaLogProb[$n] = log($p); } // ============================================================ // 子模型 2: 号码级马尔可夫转移概率 // ============================================================ $numberMarkov = $this->_buildNumberMarkovMatrix($historyAsc, $timeWeights); // 上一期号码的条件转移对数概率 $markovLogProb = []; $lastNum = $lastSpecial; for ($n = 1; $n <= 49; $n++) { $p = $numberMarkov['prob'][$lastNum][$n] ?? (1.0 / 49); $p = max($p, 0.0005); $markovLogProb[$n] = log($p); } // ============================================================ // 子模型 3: 经验 CDF 遗漏回归(继承 V3 精华) // ============================================================ // 统计每个号码的当前遗漏和历史上出现时的遗漏值分布 $omitCount = array_fill(1, 49, $totalDraws); $lastAppear = array_fill(1, 49, -1); $omitHistoryAll = []; // 所有号码的历史遗漏值池 foreach ($historyAsc as $idx => $row) { $num = (int)$row['num7']; if ($num < 1 || $num > 49) continue; if ($lastAppear[$num] >= 0) { $omitHistoryAll[] = $idx - $lastAppear[$num]; } else { $omitHistoryAll[] = $idx + 1; } $lastAppear[$num] = $idx; $omitCount[$num] = $totalDraws - 1 - $idx; } foreach ($omitCount as $n => $v) { if ($lastAppear[$n] < 0) $omitCount[$n] = $totalDraws; } sort($omitHistoryAll); $omitLogProb = []; for ($n = 1; $n <= 49; $n++) { $score = $this->_calcOmitScoreEmpirical($omitCount[$n], $omitHistoryAll, []); $omitLogProb[$n] = log(max($score / 100.0, 0.002)); } // ============================================================ // 子模型 4: 历史形态匹配 (Pattern Similarity Matching) // ============================================================ $patternResult = $this->_findSimilarPatternsV4($historyAsc, $totalDraws, $zoneMap, $isOddMap, $isBigMap, $colorKeyMap); // ============================================================ // 子模型 5: 周期性自相关检测 // ============================================================ $cyclicalLogProb = $this->_detectCyclicalPatternsV4($historyAsc, $totalDraws); // ============================================================ // 子模型 6: 属性平衡 (奇偶/大小/区域/波色) // ============================================================ // 使用时间衰减权重统计近期各属性出现比例 $attrCounts = [ 'odd' => 0, 'even' => 0, 'big' => 0, 'small' => 0, 'zone' => [0, 0, 0, 0, 0], 'color' => ['红' => 0, '蓝' => 0, '绿' => 0] ]; foreach ($historyAsc as $idx => $row) { $num = (int)$row['num7']; if ($num < 1 || $num > 49) continue; $w = $timeWeights[$idx]; $attrCounts['odd'] += ($isOddMap[$num] ? $w : 0); $attrCounts['even'] += ($isOddMap[$num] ? 0 : $w); $attrCounts['big'] += ($isBigMap[$num] ? $w : 0); $attrCounts['small'] += ($isBigMap[$num] ? 0 : $w); $attrCounts['zone'][$zoneMap[$num]] += $w; $ck = $colorKeyMap[$num]; if ($ck) $attrCounts['color'][$ck] += $w; } $totalWeight = array_sum($timeWeights); $attrLogProb = []; for ($n = 1; $n <= 49; $n++) { $logP = 0; // 奇偶平衡: 期望各50% $oddW = $attrCounts['odd'] + $attrCounts['even']; $oddRatio = $oddW > 0 ? $attrCounts['odd'] / $oddW : 0.5; $logP += log(max($isOddMap[$n] ? (1 - $oddRatio + 0.5) : ($oddRatio + 0.5), 0.4) / 1.5); // 大小平衡 $bsW = $attrCounts['big'] + $attrCounts['small']; $bigRatio = $bsW > 0 ? $attrCounts['big'] / $bsW : 0.5; $logP += log(max($isBigMap[$n] ? (1 - $bigRatio + 0.5) : ($bigRatio + 0.5), 0.4) / 1.5); // 区域平衡 $zoneTotal = array_sum($attrCounts['zone']); $zoneRatio = $zoneTotal > 0 ? $attrCounts['zone'][$zoneMap[$n]] / $zoneTotal : 0.2; $logP += log(max(1 - $zoneRatio + 0.2, 0.3) / 1.2); // 波色平衡 $colorTotal = array_sum($attrCounts['color']); $colorRatio = $colorTotal > 0 ? ($attrCounts['color'][$colorKeyMap[$n]] ?? 0) / $colorTotal : 0.333; $logP += log(max(1 - $colorRatio + 0.33, 0.3) / 1.33); $attrLogProb[$n] = $logP; } // ============================================================ // 自适应集成权重:基于滚动回测性能调整 // ============================================================ if (!$targetExpect && !$skipBacktest && $totalDraws >= 60) { $weights = $this->_adaptEnsembleWeightsV4($historyAsc, $timeWeights, $totalDraws, $weights, $ewmaLogProb, $markovLogProb, $omitLogProb, $patternResult, $cyclicalLogProb, $attrLogProb); } // —— 归一化权重 —— $wSum = array_sum($weights); if ($wSum > 0) { foreach ($weights as $k => $v) { $weights[$k] = $v / $wSum; } } // ============================================================ // 贝叶斯对数似然融合 // logPosterior(n) = Σ w_i * logP_i(n) (省略归一化常数) // 然后 Softmax 转化为概率分布 // ============================================================ $logPosterior = []; $maxLog = -INF; for ($n = 1; $n <= 49; $n++) { $lp = 0; $lp += $weights['freq_ewma'] * $ewmaLogProb[$n]; $lp += $weights['markov_number'] * $markovLogProb[$n]; $lp += $weights['omit_empirical'] * $omitLogProb[$n]; $lp += $weights['pattern_match'] * $patternResult['logprob'][$n]; $lp += $weights['cyclical'] * $cyclicalLogProb[$n]; $lp += $weights['attr_balance'] * $attrLogProb[$n]; $logPosterior[$n] = $lp; if ($lp > $maxLog) $maxLog = $lp; } // Softmax 稳定计算 $softmaxSum = 0; $softmaxRaw = []; for ($n = 1; $n <= 49; $n++) { $v = exp($logPosterior[$n] - $maxLog); $softmaxRaw[$n] = $v; $softmaxSum += $v; } $probabilities = []; for ($n = 1; $n <= 49; $n++) { $probabilities[$n] = $softmaxRaw[$n] / max($softmaxSum, 0.0001); } // —— 构建结果 —— $scores = []; for ($n = 1; $n <= 49; $n++) { $scores[] = [ 'num' => $n, 'prob' => round($probabilities[$n], 6), 'score' => round($probabilities[$n] * 100, 2), 'color' => $colorMap[$n] ?? '', 'animal' => $animalMap[$n] ?? '', 'detail' => [ 'freq_ewma' => round(exp($ewmaLogProb[$n]) * 100, 2), 'markov_number' => round(exp($markovLogProb[$n]) * 100, 2), 'omit_empirical' => round(exp($omitLogProb[$n]) * 100, 2), 'pattern_match' => round(exp($patternResult['logprob'][$n]) * 100, 2), 'cyclical' => round(exp($cyclicalLogProb[$n]) * 100, 2), 'attr_balance' => round(exp($attrLogProb[$n]) * 100, 2), 'omit_count' => $omitCount[$n], 'is_odd' => $isOddMap[$n], 'is_big' => $isBigMap[$n], 'zone' => $zoneMap[$n], ] ]; } // 按概率降序 usort($scores, function ($a, $b) { return $b['prob'] <=> $a['prob']; }); $predictions = array_slice($scores, 0, 5); // 回测 $backtest = $skipBacktest ? null : $this->_runBacktestV4($periods, $weights, $backtestCount, $cutoffTime); // 置信度 $confidence = $this->_calculateConfidenceV4($predictions, $backtest); // 命中验证 $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'] ]; } // 分析摘要 $analysis = [ 'version' => 'V4', 'algorithm' => 'Bayesian Ensemble with Time-Decay', 'last_special' => $lastSpecial, 'last_expect' => $lastExpect, 'history_count' => $totalDraws, 'weights' => $weights, 'weights_adaptive' => !$targetExpect && !$skipBacktest && $totalDraws >= 60, 'half_life' => $halfLife, 'pattern_match_info' => [ 'top_k' => $patternResult['top_k'], 'best_similarity' => $patternResult['best_similarity'], 'matched_segments' => $patternResult['segment_count'], ], 'cyclical_detected' => $this->_summarizeCyclicalV4($cyclicalLogProb), ]; return [ 'predictions' => $predictions, 'last_special' => $lastSpecial, 'last_expect' => $lastExpect, 'analysis' => $analysis, 'actual_result' => $actualResult, 'hit_info' => $hitInfo, 'backtest' => $backtest, 'confidence' => $confidence, 'probabilities' => $probabilities, // 完整 49 个号码概率分布 ]; } /** * 构建号码级马尔可夫转移矩阵(带时间衰减和拉普拉斯平滑) * * 直接建模 P(num_t = j | num_{t-1} = i),49×49 状态空间 * 使用时间衰减权重 + 自适应 K近邻平滑 * * @param array $historyAsc 升序历史数据 * @param array $timeWeights 时间衰减权重(与 historyAsc 索引对齐) * @return array ['matrix' => count, 'prob' => probability] */ private function _buildNumberMarkovMatrix($historyAsc, $timeWeights) { $matrix = array_fill(1, 49, array_fill(1, 49, 0.0)); $rowTotal = array_fill(1, 49, 0.0); $n = count($historyAsc); for ($i = 0; $i < $n - 1; $i++) { $from = (int)$historyAsc[$i]['num7']; $to = (int)$historyAsc[$i + 1]['num7']; if ($from < 1 || $from > 49 || $to < 1 || $to > 49) continue; $w = $timeWeights[$i]; // t 时刻的权重适用于 "从 t 到 t+1" 的转移 $matrix[$from][$to] += $w; $rowTotal[$from] += $w; } // 自适应平滑:K近邻平滑 (K=3) $k = 3; $prob = array_fill(1, 49, array_fill(1, 49, 0.0)); for ($i = 1; $i <= 49; $i++) { $rt = $rowTotal[$i]; // 对每个目标号码,借用邻近号码的转移计数做平滑 $smoothCount = []; for ($j = 1; $j <= 49; $j++) { $neighborSum = 0; $neighborCount = 0; // 取 i 左右各 k 个邻居的转移计数 for ($di = -$k; $di <= $k; $di++) { $ni = $i + $di; if ($ni >= 1 && $ni <= 49 && $ni != $i) { $neighborSum += $matrix[$ni][$j]; $neighborCount++; } } $neighborAvg = $neighborCount > 0 ? $neighborSum / $neighborCount : 0; // 综合自身计数 + 邻居平均 $smoothCount[$j] = $matrix[$i][$j] + $neighborAvg * 0.3; } $smoothTotal = array_sum($smoothCount) + 49; // +49 拉普拉斯 for ($j = 1; $j <= 49; $j++) { $prob[$i][$j] = ($smoothCount[$j] + 1) / max($smoothTotal, 1); } } return [ 'matrix' => $matrix, 'prob' => $prob, 'row_total' => $rowTotal ]; } /** * 历史形态匹配:在特征空间找与最近片段最相似的历史片段 * * 特征向量: [zone_seq(3), oddeven_seq(3), bigsmall_seq(3), color_seq(3)] * 共 4×3 = 12 维特征向量,用余弦相似度匹配 * * 找到 top-K 最相似的历史片段后,取其后续号码的经验分布作为预测 * * @param array $historyAsc 升序历史 * @param int $totalDraws 总期数 * @param array $zoneMap 号码->区域 * @param array $isOddMap 号码->是否奇数 * @param array $isBigMap 号码->是否大号 * @param array $colorKeyMap 号码->波色 * @return array ['logprob' => [], 'top_k' => int, 'best_similarity' => float, 'segment_count' => int] */ private function _findSimilarPatternsV4($historyAsc, $totalDraws, $zoneMap, $isOddMap, $isBigMap, $colorKeyMap) { $patternLen = 3; // 用最近3期做形态匹配 $topK = 15; // 取最相似15个片段 $minSegDist = 3; // 匹配片段不能与当前片段重叠 if ($totalDraws < $patternLen + $minSegDist + 5) { // 数据不足,返回均匀分布 $logprob = []; for ($n = 1; $n <= 49; $n++) { $logprob[$n] = log(1.0 / 49); } return ['logprob' => $logprob, 'top_k' => 0, 'best_similarity' => 0, 'segment_count' => 0]; } // 提取最近 patternLen 期的特征向量 $recentFeatures = []; $recentStart = $totalDraws - $patternLen; for ($i = $recentStart; $i < $totalDraws; $i++) { $num = (int)$historyAsc[$i]['num7']; if ($num < 1 || $num > 49) continue; $recentFeatures[] = [ 'zone' => $zoneMap[$num], 'oddeven' => $isOddMap[$num] ? 1 : 0, 'bigsmall' => $isBigMap[$num] ? 1 : 0, 'color' => $colorKeyMap[$num] === '红' ? 0 : ($colorKeyMap[$num] === '蓝' ? 1 : 2), ]; } // 在历史中扫描相似片段 $similarities = []; $maxScanStart = $totalDraws - $patternLen - $minSegDist; for ($start = 0; $start < $maxScanStart; $start++) { // 提取候选片段的特征向量 $candFeatures = []; for ($i = $start; $i < $start + $patternLen; $i++) { $num = (int)$historyAsc[$i]['num7']; if ($num < 1 || $num > 49) continue; $candFeatures[] = [ 'zone' => $zoneMap[$num], 'oddeven' => $isOddMap[$num] ? 1 : 0, 'bigsmall' => $isBigMap[$num] ? 1 : 0, 'color' => $colorKeyMap[$num] === '红' ? 0 : ($colorKeyMap[$num] === '蓝' ? 1 : 2), ]; } // 计算余弦相似度 $similarity = $this->_cosineSimilarityV4($recentFeatures, $candFeatures); $similarities[] = ['start' => $start, 'sim' => $similarity]; } // 按相似度降序排序,取 topK usort($similarities, function ($a, $b) { return $b['sim'] <=> $a['sim']; }); $topMatches = array_slice($similarities, 0, $topK); // 基于匹配片段的后续号码分布 $nextNumCounts = array_fill(1, 49, 0.0); $totalCount = 0; foreach ($topMatches as $match) { $nextPos = $match['start'] + $patternLen; if ($nextPos < $totalDraws) { $nextNum = (int)$historyAsc[$nextPos]['num7']; if ($nextNum >= 1 && $nextNum <= 49) { // 用相似度作为权重 $weight = max($match['sim'], 0.01); $nextNumCounts[$nextNum] += $weight; $totalCount += $weight; } } } // 转换为对数概率 $logprob = []; $uniformLog = log(1.0 / 49); for ($n = 1; $n <= 49; $n++) { if ($totalCount > 0 && $nextNumCounts[$n] > 0) { $p = $nextNumCounts[$n] / $totalCount; // 与均匀分布做加权平均(贝叶斯收缩) $pShrunk = $p * 0.7 + (1.0 / 49) * 0.3; $logprob[$n] = log(max($pShrunk, 0.0005)); } else { $logprob[$n] = $uniformLog; } } $bestSim = !empty($topMatches) ? $topMatches[0]['sim'] : 0; return [ 'logprob' => $logprob, 'top_k' => $topK, 'best_similarity'=> round($bestSim, 4), 'segment_count' => count($topMatches) ]; } /** * 计算两个特征序列的余弦相似度 * * @param array $seq1 特征向量数组 [{zone, oddeven, bigsmall, color}, ...] * @param array $seq2 同上 * @return float 余弦相似度 [0, 1] */ private function _cosineSimilarityV4($seq1, $seq2) { if (count($seq1) !== count($seq2) || empty($seq1)) return 0; // 将每个位置的4维特征展平 $vec1 = []; $vec2 = []; $zoneScale = 0.2; // zone 归一化到 [0,1] foreach ($seq1 as $f) { $vec1[] = $f['zone'] * $zoneScale; $vec1[] = $f['oddeven']; $vec1[] = $f['bigsmall']; $vec1[] = $f['color'] / 2.0; // 0, 0.5, 1.0 } foreach ($seq2 as $f) { $vec2[] = $f['zone'] * $zoneScale; $vec2[] = $f['oddeven']; $vec2[] = $f['bigsmall']; $vec2[] = $f['color'] / 2.0; } $dot = 0; $norm1 = 0; $norm2 = 0; $len = count($vec1); for ($i = 0; $i < $len; $i++) { $dot += $vec1[$i] * $vec2[$i]; $norm1 += $vec1[$i] * $vec1[$i]; $norm2 += $vec2[$i] * $vec2[$i]; } $denom = sqrt(max($norm1, 0.0001)) * sqrt(max($norm2, 0.0001)); return $denom > 0 ? max(0, $dot / $denom) : 0; } /** * 周期性自相关检测 * * 对每个号码,计算不同滞后期的自相关函数 (ACF) * 检测是否存在显著的周期性出现模式 * 例如某个号码每隔约 N 期出现一次的模式 * * @param array $historyAsc 升序历史 * @param int $totalDraws * @return array 每个号码的对数周期性得分 */ private function _detectCyclicalPatternsV4($historyAsc, $totalDraws) { // 构建每个号码的出现序列 (0/1 向量) $appearances = array_fill(1, 49, array_fill(0, $totalDraws, 0)); foreach ($historyAsc as $idx => $row) { $num = (int)$row['num7']; if ($num >= 1 && $num <= 49) { $appearances[$num][$idx] = 1; } } // 搜索的滞后期范围(从5期到 totalDraws/4 期) $minLag = 5; $maxLag = min((int)($totalDraws / 4), 60); $acfThreshold = 0.12; // 自相关系数阈值 $cyclicalScores = []; for ($n = 1; $n <= 49; $n++) { $seq = $appearances[$n]; $mean = array_sum($seq) / $totalDraws; if ($mean < 0.005) { // 几乎没出现过的号码,无周期性可言 $cyclicalScores[$n] = 0; continue; } // 计算方差 $variance = 0; for ($t = 0; $t < $totalDraws; $t++) { $variance += ($seq[$t] - $mean) * ($seq[$t] - $mean); } $variance /= $totalDraws; if ($variance < 0.0001) { $cyclicalScores[$n] = 0; continue; } $bestAcf = 0; $bestLag = 0; for ($lag = $minLag; $lag <= $maxLag; $lag++) { $cov = 0; $count = 0; for ($t = 0; $t < $totalDraws - $lag; $t++) { $cov += ($seq[$t] - $mean) * ($seq[$t + $lag] - $mean); $count++; } if ($count > 0) { $acf = $cov / ($count * $variance); $acf = abs($acf); if ($acf > $bestAcf) { $bestAcf = $acf; $bestLag = $lag; } } } // 周期性得分 = 最小(最佳ACF / 阈值, 1) * 100 $score = $bestAcf > $acfThreshold ? min($bestAcf / ($acfThreshold * 3), 1.0) * 100 : 0; $cyclicalScores[$n] = $score; } // 转换为对数概率(周期性高的号码获得更高概率) $logProb = []; $uniformLog = log(1.0 / 49); // 找到有显著周期性的号码 $significantNums = []; foreach ($cyclicalScores as $n => $score) { if ($score > 5) $significantNums[] = $n; } for ($n = 1; $n <= 49; $n++) { $score = $cyclicalScores[$n]; if ($score > 0) { // 周期性得分映射到概率乘数 [1, 3] $multiplier = 1 + ($score / 100) * 2; // 最近一次出现距今 $lastAppear = 0; for ($t = $totalDraws - 1; $t >= 0; $t--) { if ($appearances[$n][$t] === 1) { $lastAppear = $totalDraws - 1 - $t; break; } } // 如果当前距上次出现接近周期长度,额外加分 $phaseBonus = 0; if ($bestLag > 0 && $lastAppear > 0) { $phaseDiff = abs($lastAppear - $bestLag) / max($bestLag, 1); if ($phaseDiff < 0.3) $phaseBonus = (1 - $phaseDiff) * 0.5; } $pBoost = (1.0 / 49) * $multiplier * (1 + $phaseBonus); $logProb[$n] = log(max($pBoost, 0.0005)); } else { $logProb[$n] = $uniformLog; } } // 归一化对数概率 $maxLp = max($logProb); $lpSum = 0; $expLp = []; for ($n = 1; $n <= 49; $n++) { $expLp[$n] = exp($logProb[$n] - $maxLp); $lpSum += $expLp[$n]; } for ($n = 1; $n <= 49; $n++) { $logProb[$n] = log(max($expLp[$n] / max($lpSum, 0.0001), 0.0005)); } return $logProb; } /** * 周期性检测结果摘要 */ private function _summarizeCyclicalV4($cyclicalLogProb) { $count = 0; $uniformLog = log(1.0 / 49); foreach ($cyclicalLogProb as $lp) { if ($lp > $uniformLog + 0.1) $count++; } return [ 'significant_count' => $count, 'has_cyclical' => $count > 3, ]; } /** * 自适应集成权重:滚动回测评估各子模型独立表现,更新权重 * * 策略:用最近 30 期做滚动窗口测试,计算每个子模型的独立命中率 * 用指数移动平均 (α=0.3) 更新各子模型权重 * * @return array 更新后的权重 */ private function _adaptEnsembleWeightsV4($historyAsc, $timeWeights, $totalDraws, $baseWeights, $ewmaLogProb, $markovLogProb, $omitLogProb, $patternResult, $cyclicalLogProb, $attrLogProb) { $testWindow = min(30, (int)($totalDraws * 0.15)); if ($testWindow < 10) return $baseWeights; $subModels = ['freq_ewma', 'markov_number', 'omit_empirical', 'pattern_match', 'cyclical', 'attr_balance']; $hitCounts = array_fill_keys($subModels, 0); // 获取完整历史(需要更多数据用于滚动测试) $fullHistory = $this->field('expect,num7,openTime') ->order('openTime', 'desc') ->limit($totalDraws + $testWindow) ->select(); $fullHistoryAsc = array_reverse($fullHistory); $fullN = count($fullHistoryAsc); if ($fullN < $totalDraws + $testWindow) return $baseWeights; // 对每个子模型做简化版滚动回测 $alpha = 0.3; // EMA 平滑系数 // 简化:只测试最近 testWindow 期 for ($t = $fullN - $testWindow; $t < $fullN; $t++) { $actualNum = (int)$fullHistoryAsc[$t]['num7']; if ($actualNum < 1 || $actualNum > 49) continue; // 构建该测试点的各子模型对数概率(简化版,复用已计算的全局概率) // 这里使用快速近似而非完整重算 $testLogProbs = [ 'freq_ewma' => $ewmaLogProb, 'markov_number' => $markovLogProb, 'omit_empirical' => $omitLogProb, 'pattern_match' => $patternResult['logprob'], 'cyclical' => $cyclicalLogProb, 'attr_balance' => $attrLogProb, ]; foreach ($subModels as $model) { // 取该模型 Top-5 预测 $modelScores = []; for ($n = 1; $n <= 49; $n++) { $modelScores[$n] = $testLogProbs[$model][$n]; } arsort($modelScores); $top5 = array_slice(array_keys($modelScores), 0, 5); if (in_array($actualNum, $top5)) { $hitCounts[$model]++; } } } // 计算各模型命中率 $hitRates = []; foreach ($subModels as $model) { $hitRates[$model] = $hitCounts[$model] / max($testWindow, 1); } // EMA 更新权重: new_weight = α * (hitRate / avgHitRate) + (1-α) * baseWeight $avgHitRate = array_sum($hitRates) / max(count($hitRates), 1); if ($avgHitRate < 0.01) return $baseWeights; $adapted = []; foreach ($subModels as $model) { $performanceRatio = $hitRates[$model] / $avgHitRate; $adapted[$model] = $alpha * $baseWeights[$model] * $performanceRatio + (1 - $alpha) * $baseWeights[$model]; // 限制权重范围 [0.03, 0.40] $adapted[$model] = max(0.03, min(0.40, $adapted[$model])); } return $adapted; } /** * V4 置信度评估 * * 基于三个维度: * 1. 回测命中率稳定性 (40%) * 2. Top-1 与 Top-5 概率差距 (30%) * 3. 概率分布的熵 (30%,熵越低置信度越高) * * @param array $predictions Top-5 预测 * @param array|null $backtest 回测结果 * @return array */ private function _calculateConfidenceV4($predictions, $backtest) { $confidence = []; // 维度1: 回测命中率 $backtestHitRate = $backtest['hit_rate'] ?? 0; $btScore = $backtestHitRate >= 30 ? 80 : ($backtestHitRate >= 20 ? 60 : ($backtestHitRate >= 10 ? 40 : 20)); // 维度2: Top-1 vs Top-5 概率差距 $top1Prob = $predictions[0]['prob'] ?? 0; $top5Prob = $predictions[4]['prob'] ?? 0; $probGap = $top1Prob > 0 ? ($top1Prob - $top5Prob) / $top1Prob : 0; $gapScore = $probGap > 0.3 ? 80 : ($probGap > 0.15 ? 60 : ($probGap > 0.05 ? 40 : 20)); // 维度3: 概率分布熵(需要传入完整概率分布) $entropyScore = 50; // 默认中等 $totalScore = $btScore * 0.4 + $gapScore * 0.3 + $entropyScore * 0.3; foreach ($predictions as $idx => $p) { $level = $totalScore >= 70 ? 'high' : ($totalScore >= 50 ? 'medium' : 'low'); $confidence[] = [ 'num' => $p['num'], 'level' => $level, 'score' => round($totalScore, 1) ]; // 排名越靠后置信度略降 if ($idx > 0) $totalScore *= 0.95; } return [ 'items' => $confidence, 'overall_score'=> round($totalScore, 1), 'overall_level'=> $totalScore >= 70 ? 'high' : ($totalScore >= 50 ? 'medium' : 'low'), 'data_warning' => ($backtest && ($backtest['total_tests'] ?? 0) < 30) ? '回测期数较少,置信度可能不准确' : null, ]; } /** * V4 回测 */ private function _runBacktestV4($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->getPredictionV4($periods, $weights, $targetExpect, true, 0); 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; $ndcg5 = $this->_calculateNDCG($details, 5); $mrr = $this->_calculateMRR($details); $hitDistribution = $this->_calculateHitDistribution($details); $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_sufficient' => $testCount >= 30, ]; } }