Files
amlhc/application/admin/model/History.php
T
916117771 6c1754417c feat(admin): 尾首概率弹窗增加相同明细数据
后端新增 tailSameDetail 和 headSameDetail 字段,统计相邻两期
特码尾数/首位相同的具体值及出现次数
前端弹窗新增两个明细表格展示
2026-05-02 15:41:28 +08:00

4919 lines
185 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace app\admin\model;
use think\Model;
use app\admin\model\Num;
class History extends Model
{
// 表名
protected $name = 'history';
// 自动写入时间戳字段
protected $autoWriteTimestamp = false;
// 定义时间戳字段名
protected $createTime = false;
protected $updateTime = false;
protected $deleteTime = false;
// 追加属性
protected $append = [
];
/**
* 获取走势图数据
* @param int $periods 查询最近多少期
* @param string $type 查询类型 all=全部号码 special=仅特码
* @return array {expects: [], data: [[num1,...], ...], colorMap: []}
*/
public function getTrendData($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,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首号=010-19首号=120-29首号=230-39首号=340-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 = 号码-10到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:平均值±1041.98%命中)
$avgDiff = abs($num - $normalAvg);
$avgScore = $avgDiff <= 10 ? 42 : 0;
// 规律6:尾数±250%命中)
$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:距离≤359%
$minDist = 49;
foreach ($lastNormals as $n) {
$dist = abs($num - $n);
if ($dist < $minDist) $minDist = $dist;
}
$distScore = $minDist <= 3 ? 59 : ($minDist <= 5 ? 40 : 0);
// 规律5:尾数≤250%
$tailDiff = abs($num % 10 - $normalSum % 10);
$tailDiff = min($tailDiff, 10 - $tailDiff);
$tailScore = $tailDiff <= 2 ? 50 : ($tailDiff <= 3 ? 30 : 0);
// 规律6:平均值±1042%
$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)
];
}
}