8b2590c5b5
- 完成Phase 11: predictV3算法优化研究文档,涵盖6个优化方向的技术分析 - 实现置信度评估功能,提供历史命中率、得分分布、多维度一致性置信度指标 - 扩展回测指标体系,新增NDCG@K、MRR、命中率分布等排名质量评估指标 - 优化转移概率算法,引入二阶马尔可夫链和多属性联合转移增强预测准确性 - 设计权重训练机制,支持网格搜索和遗传算法进行数据驱动的参数优化 - 集成组合特征挖掘功能,采用关联规则和序列模式发现号码间潜在关联 - 实现完整的前端交互界面,支持预测结果显示、置信度展示和回测验证功能 - 建立性能优化策略,包括预计算缓存、批量计算和降级策略保障响应速度
4833 lines
182 KiB
PHP
4833 lines
182 KiB
PHP
<?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首号=0,10-19首号=1,20-29首号=2,30-39首号=3,40-49首号=4
|
||
* @param int $periods 查询最近多少期
|
||
* @return array {heads: [], matrix: [], probabilities: [], row_totals: [], total_transitions: int}
|
||
*/
|
||
public function getHeadNumberTransition($periods = 100)
|
||
{
|
||
$history = $this
|
||
->field('expect,num7,openTime')
|
||
->order('openTime', 'desc')
|
||
->limit($periods)
|
||
->select();
|
||
|
||
if (empty($history) || count($history) < 2) {
|
||
return ['heads' => ['首号0','首号1','首号2','首号3','首号4'], 'matrix' => [], 'probabilities' => [], 'row_totals' => [], 'total_transitions' => 0];
|
||
}
|
||
|
||
$history = array_reverse($history);
|
||
|
||
$headLabels = ['首号0', '首号1', '首号2', '首号3', '首号4'];
|
||
$numHeads = 5;
|
||
$matrix = array_fill(0, $numHeads, array_fill(0, $numHeads, 0));
|
||
$rowTotals = array_fill(0, $numHeads, 0);
|
||
|
||
$getHead = function ($num) {
|
||
return intval(floor($num / 10));
|
||
};
|
||
|
||
$totalTransitions = 0;
|
||
for ($i = 0; $i < count($history) - 1; $i++) {
|
||
$currentNum = (int)$history[$i]['num7'];
|
||
$nextNum = (int)$history[$i + 1]['num7'];
|
||
if ($currentNum < 1 || $currentNum > 49 || $nextNum < 1 || $nextNum > 49) continue;
|
||
|
||
$from = $getHead($currentNum);
|
||
$to = $getHead($nextNum);
|
||
$matrix[$from][$to]++;
|
||
$rowTotals[$from]++;
|
||
$totalTransitions++;
|
||
}
|
||
|
||
$probabilities = array_fill(0, $numHeads, array_fill(0, $numHeads, 0));
|
||
for ($i = 0; $i < $numHeads; $i++) {
|
||
if ($rowTotals[$i] > 0) {
|
||
for ($j = 0; $j < $numHeads; $j++) {
|
||
$probabilities[$i][$j] = round($matrix[$i][$j] / $rowTotals[$i] * 100, 1);
|
||
}
|
||
}
|
||
}
|
||
|
||
return [
|
||
'heads' => $headLabels,
|
||
'matrix' => $matrix,
|
||
'probabilities' => $probabilities,
|
||
'row_totals' => $rowTotals,
|
||
'total_transitions' => $totalTransitions
|
||
];
|
||
}
|
||
|
||
/**
|
||
* 区域→区域转移矩阵,单元格内显示波色概率
|
||
* 统计上一期特码所在区域后,下一期特码在各区域的分布,以及每个区域内的波色占比
|
||
* @param int $periods 查询最近多少期
|
||
* @return array {zones: [], matrix: [[zone_count]], color_probs: [[{red, blue, green}]]}
|
||
*/
|
||
public function getZoneToZoneColor($periods = 100)
|
||
{
|
||
$history = $this
|
||
->field('expect,num7,openTime')
|
||
->order('openTime', 'desc')
|
||
->limit($periods)
|
||
->select();
|
||
|
||
if (empty($history) || count($history) < 2) {
|
||
return ['zones' => ['1-10','11-20','21-30','31-40','41-49'], 'matrix' => [], 'color_probs' => []];
|
||
}
|
||
|
||
$history = array_reverse($history);
|
||
$num_model = new Num();
|
||
$colorMap = $num_model->column('color', 'num');
|
||
|
||
$zoneLabels = ['1-10', '11-20', '21-30', '31-40', '41-49'];
|
||
$matrix = array_fill(0, 5, array_fill(0, 5, 0));
|
||
$colorCounts = [];
|
||
for ($i = 0; $i < 5; $i++) {
|
||
for ($j = 0; $j < 5; $j++) {
|
||
$colorCounts[$i][$j] = ['red' => 0, 'blue' => 0, 'green' => 0];
|
||
}
|
||
}
|
||
|
||
$getZone = function ($num) {
|
||
if ($num <= 10) return 0;
|
||
if ($num <= 20) return 1;
|
||
if ($num <= 30) return 2;
|
||
if ($num <= 40) return 3;
|
||
return 4;
|
||
};
|
||
|
||
for ($i = 0; $i < count($history) - 1; $i++) {
|
||
$currentNum = (int)$history[$i]['num7'];
|
||
$nextNum = (int)$history[$i + 1]['num7'];
|
||
$from = $getZone($currentNum);
|
||
$to = $getZone($nextNum);
|
||
$color = $colorMap[$nextNum] ?? '';
|
||
|
||
$matrix[$from][$to]++;
|
||
if (strpos($color, '红') !== false) $colorCounts[$from][$to]['red']++;
|
||
elseif (strpos($color, '蓝') !== false) $colorCounts[$from][$to]['blue']++;
|
||
elseif (strpos($color, '绿') !== false) $colorCounts[$from][$to]['green']++;
|
||
}
|
||
|
||
// 计算每个单元格波色概率
|
||
$colorProbs = array_fill(0, 5, array_fill(0, 5, ['red' => 0, 'blue' => 0, 'green' => 0]));
|
||
for ($i = 0; $i < 5; $i++) {
|
||
for ($j = 0; $j < 5; $j++) {
|
||
$total = $matrix[$i][$j];
|
||
if ($total > 0) {
|
||
$colorProbs[$i][$j] = [
|
||
'red' => round($colorCounts[$i][$j]['red'] / $total * 100, 1),
|
||
'blue' => round($colorCounts[$i][$j]['blue'] / $total * 100, 1),
|
||
'green' => round($colorCounts[$i][$j]['green'] / $total * 100, 1),
|
||
];
|
||
}
|
||
}
|
||
}
|
||
|
||
return [
|
||
'zones' => $zoneLabels,
|
||
'matrix' => $matrix,
|
||
'color_probs' => $colorProbs
|
||
];
|
||
}
|
||
|
||
/**
|
||
* 特码热力图数据
|
||
* @param int $periods 查询最近多少期
|
||
* @return array {expects: [], heatmap: [[x, y, value]], colors: [号码对应颜色]}
|
||
*/
|
||
public function getSpecialHeatmap($periods = 30)
|
||
{
|
||
$num_model = new Num();
|
||
$colorMap = $num_model->column('color', 'num');
|
||
|
||
// 查询最近 N 期特码数据
|
||
$history = $this
|
||
->field('expect,num7,openTime')
|
||
->order('openTime', 'desc')
|
||
->limit($periods)
|
||
->select();
|
||
|
||
if (empty($history)) {
|
||
return ['expects' => [], 'heatmap' => [], 'colors' => []];
|
||
}
|
||
|
||
// 反转数组,使数据从左到右为从远到近
|
||
$history = array_reverse($history);
|
||
$expects = [];
|
||
$heatmap = [];
|
||
|
||
// 构建热力图数据:[x_index, y_index, value]
|
||
// x_index = 期号索引(0到periods-1)
|
||
// y_index = 号码-1(0到48,号码1-49)
|
||
// value = 1(出现)或 0(未出现)
|
||
foreach ($history as $idx => $row) {
|
||
$expects[] = (string)$row['expect'];
|
||
$specialNum = (int)$row['num7'];
|
||
// 标记该期特码号码出现
|
||
if ($specialNum >= 1 && $specialNum <= 49) {
|
||
$heatmap[] = [$idx, $specialNum - 1, 1];
|
||
}
|
||
}
|
||
|
||
// 补充号码颜色映射(索引0对应号码1)
|
||
$colors = [];
|
||
for ($num = 1; $num <= 49; $num++) {
|
||
$color = $colorMap[$num] ?? '';
|
||
if (strpos($color, '红') !== false) {
|
||
$colors[] = '#e74c3c';
|
||
} elseif (strpos($color, '蓝') !== false) {
|
||
$colors[] = '#3498db';
|
||
} elseif (strpos($color, '绿') !== false) {
|
||
$colors[] = '#2ecc71';
|
||
} else {
|
||
$colors[] = '#95a5a6';
|
||
}
|
||
}
|
||
|
||
return [
|
||
'expects' => $expects,
|
||
'heatmap' => $heatmap,
|
||
'colors' => $colors,
|
||
'nums' => range(1, 49) // 号码列表
|
||
];
|
||
}
|
||
|
||
/**
|
||
* 综合预测号码
|
||
* 基于历史多维度转移概率分析,给出号码预测建议
|
||
* @param int $periods 查询最近多少期用于转移概率统计
|
||
* @param array $weights 各维度权重配置
|
||
* @param string $targetExpect 目标期号(可选,用于验证历史预测成功率)
|
||
* @return array {predictions: [], last_special: int, analysis: {}, actual_result: {}, hit_info: {}}
|
||
*/
|
||
public function getPrediction($periods = 100, $weights = [], $targetExpect = '')
|
||
{
|
||
// 默认权重配置
|
||
$defaultWeights = [
|
||
'zone' => 0.25, // 区域转移
|
||
'zodiac' => 0.20, // 生肖转移
|
||
'tail' => 0.20, // 尾号转移
|
||
'head' => 0.15, // 首号转移
|
||
'color' => 0.10, // 波色转移
|
||
'hotcold' => 0.10 // 冷热系数
|
||
];
|
||
$weights = array_merge($defaultWeights, $weights);
|
||
|
||
// 获取号码属性映射
|
||
$num_model = new Num();
|
||
$colorMap = $num_model->column('color', 'num');
|
||
$animalMap = $num_model->column('animal', 'num');
|
||
|
||
// 辅助函数:获取号码所属维度索引
|
||
$getZoneIdx = function ($num) {
|
||
if ($num <= 10) return 0;
|
||
if ($num <= 20) return 1;
|
||
if ($num <= 30) return 2;
|
||
if ($num <= 40) return 3;
|
||
return 4;
|
||
};
|
||
|
||
$getTailIdx = function ($num) {
|
||
return $num % 10;
|
||
};
|
||
|
||
$getHeadIdx = function ($num) {
|
||
return intval(floor($num / 10));
|
||
};
|
||
|
||
$getZodiacIdx = function ($num) use ($animalMap) {
|
||
$animalLabels = ['鼠', '牛', '虎', '兔', '龙', '蛇', '马', '羊', '猴', '鸡', '狗', '猪'];
|
||
$animal = $animalMap[$num] ?? '';
|
||
$idx = array_search($animal, $animalLabels);
|
||
return $idx === false ? -1 : $idx;
|
||
};
|
||
|
||
$getColorIdx = function ($num) use ($colorMap) {
|
||
$color = $colorMap[$num] ?? '';
|
||
if (strpos($color, '红') !== false) return 0;
|
||
if (strpos($color, '蓝') !== false) return 1;
|
||
if (strpos($color, '绿') !== false) return 2;
|
||
return -1;
|
||
};
|
||
|
||
// 确定预测基准期和目标期
|
||
$actualResult = null;
|
||
$lastSpecial = 0;
|
||
$lastExpect = '';
|
||
$cutoffTime = null;
|
||
|
||
if ($targetExpect) {
|
||
// 查找目标期号
|
||
$targetRow = $this->where('expect', $targetExpect)->find();
|
||
if (!$targetRow) {
|
||
return ['predictions' => [], 'error' => '期号不存在', 'target_expect' => $targetExpect];
|
||
}
|
||
$cutoffTime = $targetRow['openTime'];
|
||
$actualResult = [
|
||
'expect' => (string)$targetRow['expect'],
|
||
'num7' => (int)$targetRow['num7'],
|
||
'color' => $colorMap[$targetRow['num7']] ?? '',
|
||
'animal' => $animalMap[$targetRow['num7']] ?? '',
|
||
'openTime' => $targetRow['openTime']
|
||
];
|
||
|
||
// 获取目标期号前一期的特码作为预测基准
|
||
$prevRow = $this->where('openTime', '<', $cutoffTime)->order('openTime', 'desc')->limit(1)->find();
|
||
if (!$prevRow) {
|
||
return ['predictions' => [], 'error' => '目标期号之前没有历史数据', 'target_expect' => $targetExpect];
|
||
}
|
||
$lastSpecial = (int)$prevRow['num7'];
|
||
$lastExpect = (string)$prevRow['expect'];
|
||
} else {
|
||
// 使用最新一期作为预测基准
|
||
$latest = $this->field('expect,num7,openTime')->order('openTime', 'desc')->limit(1)->find();
|
||
if (!$latest) {
|
||
return ['predictions' => [], 'last_special' => 0, 'analysis' => []];
|
||
}
|
||
$lastSpecial = (int)$latest['num7'];
|
||
$lastExpect = (string)$latest['expect'];
|
||
}
|
||
|
||
// 计算指定时间点之前的转移概率
|
||
$zoneData = $this->_calcTransitionAtTime('zone', $periods, $cutoffTime);
|
||
$zodiacData = $this->_calcTransitionAtTime('zodiac', $periods, $cutoffTime);
|
||
$tailData = $this->_calcTransitionAtTime('tail', $periods, $cutoffTime);
|
||
$headData = $this->_calcTransitionAtTime('head', $periods, $cutoffTime);
|
||
$colorData = $this->_calcTransitionAtTime('color', $periods, $cutoffTime);
|
||
|
||
// 计算指定时间点的冷热状态
|
||
$hotcoldStatus = $this->_calcHotcoldAtTime($lastSpecial, 30, $cutoffTime);
|
||
|
||
// 获取上一期特码所在各维度
|
||
$lastZone = $getZoneIdx($lastSpecial);
|
||
$lastTail = $getTailIdx($lastSpecial);
|
||
$lastHead = $getHeadIdx($lastSpecial);
|
||
$lastZodiac = $getZodiacIdx($lastSpecial);
|
||
$lastColor = $getColorIdx($lastSpecial);
|
||
|
||
// 计算每个号码的综合预测得分
|
||
$scores = [];
|
||
$analysis = [
|
||
'last_special' => $lastSpecial,
|
||
'last_expect' => $lastExpect,
|
||
'last_zone' => $zoneData['zones'][$lastZone] ?? '',
|
||
'last_zodiac' => $zodiacData['animals'][$lastZodiac] ?? '',
|
||
'last_tail' => $tailData['tails'][$lastTail] ?? '',
|
||
'last_head' => $headData['heads'][$lastHead] ?? '',
|
||
'last_color' => $colorData['colors'][$lastColor] ?? '',
|
||
'weights' => $weights,
|
||
'target_expect' => $targetExpect
|
||
];
|
||
|
||
for ($num = 1; $num <= 49; $num++) {
|
||
$totalScore = 0;
|
||
$detail = [];
|
||
|
||
// 区域转移得分
|
||
$targetZone = $getZoneIdx($num);
|
||
if ($lastZone >= 0 && $lastZone < 5 && isset($zoneData['probabilities'][$lastZone][$targetZone])) {
|
||
$zoneProb = $zoneData['probabilities'][$lastZone][$targetZone];
|
||
$zoneScore = $zoneProb * $weights['zone'];
|
||
$totalScore += $zoneScore;
|
||
$detail['zone_prob'] = $zoneProb;
|
||
}
|
||
|
||
// 生肖转移得分
|
||
$targetZodiac = $getZodiacIdx($num);
|
||
if ($lastZodiac >= 0 && $lastZodiac < 12 && $targetZodiac >= 0 && isset($zodiacData['probabilities'][$lastZodiac][$targetZodiac])) {
|
||
$zodiacProb = $zodiacData['probabilities'][$lastZodiac][$targetZodiac];
|
||
$zodiacScore = $zodiacProb * $weights['zodiac'];
|
||
$totalScore += $zodiacScore;
|
||
$detail['zodiac_prob'] = $zodiacProb;
|
||
}
|
||
|
||
// 尾号转移得分
|
||
$targetTail = $getTailIdx($num);
|
||
if ($lastTail >= 0 && $lastTail < 10 && isset($tailData['probabilities'][$lastTail][$targetTail])) {
|
||
$tailProb = $tailData['probabilities'][$lastTail][$targetTail];
|
||
$tailScore = $tailProb * $weights['tail'];
|
||
$totalScore += $tailScore;
|
||
$detail['tail_prob'] = $tailProb;
|
||
}
|
||
|
||
// 首号转移得分
|
||
$targetHead = $getHeadIdx($num);
|
||
if ($lastHead >= 0 && $lastHead < 5 && isset($headData['probabilities'][$lastHead][$targetHead])) {
|
||
$headProb = $headData['probabilities'][$lastHead][$targetHead];
|
||
$headScore = $headProb * $weights['head'];
|
||
$totalScore += $headScore;
|
||
$detail['head_prob'] = $headProb;
|
||
}
|
||
|
||
// 波色转移得分
|
||
$targetColor = $getColorIdx($num);
|
||
if ($lastColor >= 0 && $lastColor < 3 && $targetColor >= 0 && isset($colorData['probabilities'][$lastColor][$targetColor])) {
|
||
$colorProb = $colorData['probabilities'][$lastColor][$targetColor];
|
||
$colorScore = $colorProb * $weights['color'];
|
||
$totalScore += $colorScore;
|
||
$detail['color_prob'] = $colorProb;
|
||
}
|
||
|
||
// 冷热系数得分
|
||
$status = $hotcoldStatus[$num] ?? 'warm';
|
||
$hotcoldFactor = 0;
|
||
if ($status === 'cold') {
|
||
$hotcoldFactor = 100;
|
||
} elseif ($status === 'warm') {
|
||
$hotcoldFactor = 50;
|
||
} else {
|
||
$hotcoldFactor = 20;
|
||
}
|
||
$hotcoldScore = $hotcoldFactor * $weights['hotcold'];
|
||
$totalScore += $hotcoldScore;
|
||
$detail['hotcold_status'] = $status;
|
||
$detail['hotcold_factor'] = $hotcoldFactor;
|
||
|
||
$scores[$num] = [
|
||
'num' => $num,
|
||
'score' => round($totalScore, 2),
|
||
'color' => $colorMap[$num] ?? '',
|
||
'animal' => $animalMap[$num] ?? '',
|
||
'detail' => $detail
|
||
];
|
||
}
|
||
|
||
// 按得分降序排序
|
||
$predictions = array_values($scores);
|
||
usort($predictions, function ($a, $b) {
|
||
return $b['score'] - $a['score'];
|
||
});
|
||
|
||
// 只返回得分最高的前5个号码
|
||
$predictions = array_slice($predictions, 0, 5);
|
||
|
||
// 计算命中情况
|
||
$hitInfo = null;
|
||
if ($actualResult) {
|
||
$hitRank = -1;
|
||
foreach ($predictions as $idx => $p) {
|
||
if ($p['num'] === $actualResult['num7']) {
|
||
$hitRank = $idx + 1;
|
||
break;
|
||
}
|
||
}
|
||
$hitInfo = [
|
||
'hit' => $hitRank > 0,
|
||
'rank' => $hitRank,
|
||
'actual_num' => $actualResult['num7'],
|
||
'actual_color' => $actualResult['color'],
|
||
'actual_animal' => $actualResult['animal'],
|
||
'actual_expect' => $actualResult['expect']
|
||
];
|
||
}
|
||
|
||
return [
|
||
'predictions' => $predictions,
|
||
'last_special' => $lastSpecial,
|
||
'last_expect' => $lastExpect,
|
||
'analysis' => $analysis,
|
||
'actual_result' => $actualResult,
|
||
'hit_info' => $hitInfo
|
||
];
|
||
}
|
||
|
||
/**
|
||
* 计算指定时间点的转移概率
|
||
* @param string $type 转移类型: zone/zodiac/tail/head/color
|
||
* @param int $periods 统计期数
|
||
* @param string|null $cutoffTime 截断时间(该时间之后的数据不参与统计)
|
||
* @return array
|
||
*/
|
||
private function _calcTransitionAtTime($type, $periods, $cutoffTime = null)
|
||
{
|
||
$query = $this->field('expect,num7,openTime')->order('openTime', 'desc');
|
||
if ($cutoffTime) {
|
||
$query = $query->where('openTime', '<', $cutoffTime);
|
||
}
|
||
$history = $query->limit($periods)->select();
|
||
|
||
if (empty($history) || count($history) < 2) {
|
||
return $this->_getEmptyTransition($type);
|
||
}
|
||
|
||
$history = array_reverse($history);
|
||
|
||
$num_model = new Num();
|
||
$colorMap = $num_model->column('color', 'num');
|
||
$animalMap = $num_model->column('animal', 'num');
|
||
|
||
$getZoneIdx = function ($num) {
|
||
if ($num <= 10) return 0;
|
||
if ($num <= 20) return 1;
|
||
if ($num <= 30) return 2;
|
||
if ($num <= 40) return 3;
|
||
return 4;
|
||
};
|
||
|
||
$getTailIdx = function ($num) {
|
||
return $num % 10;
|
||
};
|
||
|
||
$getHeadIdx = function ($num) {
|
||
return intval(floor($num / 10));
|
||
};
|
||
|
||
$getZodiacIdx = function ($num) use ($animalMap) {
|
||
$animalLabels = ['鼠', '牛', '虎', '兔', '龙', '蛇', '马', '羊', '猴', '鸡', '狗', '猪'];
|
||
$animal = $animalMap[$num] ?? '';
|
||
$idx = array_search($animal, $animalLabels);
|
||
return $idx === false ? -1 : $idx;
|
||
};
|
||
|
||
$getColorIdx = function ($num) use ($colorMap) {
|
||
$color = $colorMap[$num] ?? '';
|
||
if (strpos($color, '红') !== false) return 0;
|
||
if (strpos($color, '蓝') !== false) return 1;
|
||
if (strpos($color, '绿') !== false) return 2;
|
||
return -1;
|
||
};
|
||
|
||
switch ($type) {
|
||
case 'zone':
|
||
$labels = ['1-10', '11-20', '21-30', '31-40', '41-49'];
|
||
$size = 5;
|
||
$getIdx = $getZoneIdx;
|
||
break;
|
||
case 'zodiac':
|
||
$labels = ['鼠', '牛', '虎', '兔', '龙', '蛇', '马', '羊', '猴', '鸡', '狗', '猪'];
|
||
$size = 12;
|
||
$getIdx = $getZodiacIdx;
|
||
break;
|
||
case 'tail':
|
||
$labels = ['尾0', '尾1', '尾2', '尾3', '尾4', '尾5', '尾6', '尾7', '尾8', '尾9'];
|
||
$size = 10;
|
||
$getIdx = $getTailIdx;
|
||
break;
|
||
case 'head':
|
||
$labels = ['首号0', '首号1', '首号2', '首号3', '首号4'];
|
||
$size = 5;
|
||
$getIdx = $getHeadIdx;
|
||
break;
|
||
case 'color':
|
||
$labels = ['红波', '蓝波', '绿波'];
|
||
$size = 3;
|
||
$getIdx = $getColorIdx;
|
||
break;
|
||
default:
|
||
return [];
|
||
}
|
||
|
||
$keyField = $type === 'zone' ? 'zones' : ($type === 'zodiac' ? 'animals' : ($type === 'tail' ? 'tails' : ($type === 'head' ? 'heads' : 'colors')));
|
||
$matrix = array_fill(0, $size, array_fill(0, $size, 0));
|
||
$rowTotals = array_fill(0, $size, 0);
|
||
$totalTransitions = 0;
|
||
|
||
for ($i = 0; $i < count($history) - 1; $i++) {
|
||
$currentNum = (int)$history[$i]['num7'];
|
||
$nextNum = (int)$history[$i + 1]['num7'];
|
||
$from = $getIdx($currentNum);
|
||
$to = $getIdx($nextNum);
|
||
if ($from < 0 || $to < 0) continue;
|
||
$matrix[$from][$to]++;
|
||
$rowTotals[$from]++;
|
||
$totalTransitions++;
|
||
}
|
||
|
||
$probabilities = array_fill(0, $size, array_fill(0, $size, 0));
|
||
for ($i = 0; $i < $size; $i++) {
|
||
if ($rowTotals[$i] > 0) {
|
||
for ($j = 0; $j < $size; $j++) {
|
||
$probabilities[$i][$j] = round($matrix[$i][$j] / $rowTotals[$i] * 100, 1);
|
||
}
|
||
}
|
||
}
|
||
|
||
return [
|
||
$keyField => $labels,
|
||
'matrix' => $matrix,
|
||
'probabilities' => $probabilities,
|
||
'row_totals' => $rowTotals,
|
||
'total_transitions' => $totalTransitions
|
||
];
|
||
}
|
||
|
||
/**
|
||
* 获取空的转移概率结构
|
||
*/
|
||
private function _getEmptyTransition($type)
|
||
{
|
||
switch ($type) {
|
||
case 'zone':
|
||
return ['zones' => ['1-10','11-20','21-30','31-40','41-49'], 'matrix' => [], 'probabilities' => [], 'row_totals' => [], 'total_transitions' => 0];
|
||
case 'zodiac':
|
||
return ['animals' => ['鼠','牛','虎','兔','龙','蛇','马','羊','猴','鸡','狗','猪'], 'matrix' => [], 'probabilities' => [], 'row_totals' => [], 'total_transitions' => 0];
|
||
case 'tail':
|
||
return ['tails' => ['尾0','尾1','尾2','尾3','尾4','尾5','尾6','尾7','尾8','尾9'], 'matrix' => [], 'probabilities' => [], 'row_totals' => [], 'total_transitions' => 0];
|
||
case 'head':
|
||
return ['heads' => ['首号0','首号1','首号2','首号3','首号4'], 'matrix' => [], 'probabilities' => [], 'row_totals' => [], 'total_transitions' => 0];
|
||
case 'color':
|
||
return ['colors' => ['红波','蓝波','绿波'], 'matrix' => [], 'probabilities' => [], 'row_totals' => [], 'total_transitions' => 0];
|
||
default:
|
||
return [];
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 计算指定时间点的冷热状态
|
||
* @param int $lastSpecial 前一期特码(用于确定统计截止点)
|
||
* @param int $lookback 回看期数
|
||
* @param string|null $cutoffTime 截断时间
|
||
* @return array 号码冷热状态映射
|
||
*/
|
||
private function _calcHotcoldAtTime($lastSpecial, $lookback, $cutoffTime = null)
|
||
{
|
||
// 找到前一期的时间点
|
||
if ($cutoffTime) {
|
||
$prevTime = $cutoffTime;
|
||
} else {
|
||
$prevRow = $this->field('openTime')->order('openTime', 'desc')->limit(1)->find();
|
||
$prevTime = $prevRow ? $prevRow['openTime'] : null;
|
||
}
|
||
|
||
if (!$prevTime) {
|
||
return [];
|
||
}
|
||
|
||
// 统计该时间点之前 lookback 期的特码出现次数
|
||
$history = $this->field('num7')
|
||
->where('openTime', '<', $prevTime)
|
||
->order('openTime', 'desc')
|
||
->limit($lookback)
|
||
->select();
|
||
|
||
$count = array_fill(1, 49, 0);
|
||
$periodCount = 0;
|
||
foreach ($history as $row) {
|
||
$num = (int)$row['num7'];
|
||
if ($num >= 1 && $num <= 49) {
|
||
$count[$num]++;
|
||
}
|
||
$periodCount++;
|
||
}
|
||
|
||
$avgCount = $periodCount > 0 ? $periodCount / 49 : 0;
|
||
$status = [];
|
||
|
||
for ($num = 1; $num <= 49; $num++) {
|
||
if ($avgCount > 0) {
|
||
if ($count[$num] > $avgCount * 1.5) {
|
||
$status[$num] = 'hot';
|
||
} elseif ($count[$num] < $avgCount * 0.5) {
|
||
$status[$num] = 'cold';
|
||
} else {
|
||
$status[$num] = 'warm';
|
||
}
|
||
} else {
|
||
$status[$num] = 'warm';
|
||
}
|
||
}
|
||
|
||
return $status;
|
||
}
|
||
|
||
/**
|
||
* 改进版智能预测算法 - 基于统计回归分析
|
||
* @param int $periods 查询最近多少期用于统计分析
|
||
* @param array $weights 各维度权重配置
|
||
* @param string $targetExpect 目标期号(可选,用于验证历史预测成功率)
|
||
* @param bool $skipBacktest 是否跳过回测(内部调用时使用)
|
||
* @param int $backtestCount 回测期数(默认50)
|
||
* @return array {predictions: [], last_special: int, analysis: {}, actual_result: {}, hit_info: {}, backtest: {}}
|
||
*/
|
||
public function getPredictionV2($periods = 200, $weights = [], $targetExpect = '', $skipBacktest = false, $backtestCount = 50)
|
||
{
|
||
// 获取号码属性映射
|
||
$num_model = new Num();
|
||
$colorMap = $num_model->column('color', 'num');
|
||
$animalMap = $num_model->column('animal', 'num');
|
||
|
||
// 默认权重配置(改进版)
|
||
$defaultWeights = [
|
||
'omit_regression' => 0.30, // 遗漏回归权重
|
||
'freq_regression' => 0.25, // 频率回归权重(已合并统计显著性)
|
||
'recent_trend' => 0.20, // 近期趋势权重
|
||
'zone_balance' => 0.10, // 区域平衡权重
|
||
'color_balance' => 0.08, // 波色平衡权重
|
||
'stat_significance' => 0.07 // 统计显著性权重(已合并到freq_regression,保留兼容)
|
||
];
|
||
$weights = array_merge($defaultWeights, $weights);
|
||
|
||
// 确定预测基准期和目标期
|
||
$actualResult = null;
|
||
$lastSpecial = 0;
|
||
$lastExpect = '';
|
||
$cutoffTime = null;
|
||
$allHistory = [];
|
||
|
||
if ($targetExpect) {
|
||
$targetRow = $this->where('expect', $targetExpect)->find();
|
||
if (!$targetRow) {
|
||
return ['predictions' => [], 'error' => '期号不存在', 'target_expect' => $targetExpect];
|
||
}
|
||
$cutoffTime = $targetRow['openTime'];
|
||
$actualResult = [
|
||
'expect' => (string)$targetRow['expect'],
|
||
'num7' => (int)$targetRow['num7'],
|
||
'color' => $colorMap[$targetRow['num7']] ?? '',
|
||
'animal' => $animalMap[$targetRow['num7']] ?? '',
|
||
'openTime' => $targetRow['openTime']
|
||
];
|
||
$prevRow = $this->where('openTime', '<', $cutoffTime)->order('openTime', 'desc')->limit(1)->find();
|
||
if (!$prevRow) {
|
||
return ['predictions' => [], 'error' => '目标期号之前没有历史数据', 'target_expect' => $targetExpect];
|
||
}
|
||
$lastSpecial = (int)$prevRow['num7'];
|
||
$lastExpect = (string)$prevRow['expect'];
|
||
$allHistory = $this->field('expect,num7,openTime')
|
||
->where('openTime', '<', $cutoffTime)
|
||
->order('openTime', 'desc')
|
||
->limit($periods)
|
||
->select();
|
||
} else {
|
||
$latest = $this->field('expect,num7,openTime')->order('openTime', 'desc')->limit(1)->find();
|
||
if (!$latest) {
|
||
return ['predictions' => [], 'last_special' => 0, 'analysis' => []];
|
||
}
|
||
$lastSpecial = (int)$latest['num7'];
|
||
$lastExpect = (string)$latest['expect'];
|
||
$allHistory = $this->field('expect,num7,openTime')
|
||
->order('openTime', 'desc')
|
||
->limit($periods)
|
||
->select();
|
||
}
|
||
|
||
if (empty($allHistory) || count($allHistory) < 30) {
|
||
return ['predictions' => [], 'error' => '历史数据不足(至少需要30期)', 'last_special' => $lastSpecial];
|
||
}
|
||
|
||
// 反转为升序(从旧到新)
|
||
$historyAsc = array_reverse($allHistory);
|
||
|
||
// ====== 1. 遗漏值分析 ======
|
||
// 计算每个号码的当前遗漏期数
|
||
$omitCount = array_fill(1, 49, count($allHistory));
|
||
$periodIdx = 0;
|
||
foreach ($allHistory as $row) {
|
||
$num = (int)$row['num7'];
|
||
if ($num >= 1 && $num <= 49 && $omitCount[$num] === count($allHistory)) {
|
||
$omitCount[$num] = $periodIdx;
|
||
}
|
||
$periodIdx++;
|
||
}
|
||
|
||
// 计算历史遗漏值分布统计
|
||
$omitHistory = [];
|
||
$omitStats = ['avg' => 0, 'max' => 0, 'std' => 0];
|
||
foreach ($allHistory as $row) {
|
||
$num = (int)$row['num7'];
|
||
// 计算该号码在开奖时的遗漏值
|
||
$tempOmit = array_fill(1, 49, count($allHistory));
|
||
$foundIdx = -1;
|
||
$idx = 0;
|
||
foreach ($allHistory as $checkRow) {
|
||
if ($checkRow['openTime'] === $row['openTime']) {
|
||
$foundIdx = $idx;
|
||
break;
|
||
}
|
||
$checkNum = (int)$checkRow['num7'];
|
||
if ($checkNum >= 1 && $checkNum <= 49 && $tempOmit[$checkNum] === count($allHistory)) {
|
||
$tempOmit[$checkNum] = $idx;
|
||
}
|
||
$idx++;
|
||
}
|
||
if ($foundIdx >= 0) {
|
||
$omitAtOpen = $tempOmit[$num];
|
||
$omitHistory[] = $omitAtOpen;
|
||
}
|
||
}
|
||
|
||
// 遗漏值统计
|
||
if (count($omitHistory) > 0) {
|
||
$omitStats['avg'] = array_sum($omitHistory) / count($omitHistory);
|
||
$omitStats['max'] = max($omitHistory);
|
||
$omitStats['std'] = $this->_calcStdDev($omitHistory);
|
||
}
|
||
|
||
// ====== 2. 频率分析 ======
|
||
// 全历史频率
|
||
$freqAll = array_fill(1, 49, 0);
|
||
foreach ($allHistory as $row) {
|
||
$num = (int)$row['num7'];
|
||
if ($num >= 1 && $num <= 49) {
|
||
$freqAll[$num]++;
|
||
}
|
||
}
|
||
$expectedFreq = count($allHistory) / 49;
|
||
|
||
// 近期频率(最近30期)
|
||
$recent30 = array_slice($allHistory, 0, 30);
|
||
$freq30 = array_fill(1, 49, 0);
|
||
foreach ($recent30 as $row) {
|
||
$num = (int)$row['num7'];
|
||
if ($num >= 1 && $num <= 49) {
|
||
$freq30[$num]++;
|
||
}
|
||
}
|
||
$expectedFreq30 = 30 / 49;
|
||
|
||
// 近期频率(最近10期)
|
||
$recent10 = array_slice($allHistory, 0, 10);
|
||
$freq10 = array_fill(1, 49, 0);
|
||
foreach ($recent10 as $row) {
|
||
$num = (int)$row['num7'];
|
||
if ($num >= 1 && $num <= 49) {
|
||
$freq10[$num]++;
|
||
}
|
||
}
|
||
$expectedFreq10 = 10 / 49;
|
||
|
||
// ====== 3. 区域分布分析 ======
|
||
$zoneLabels = ['1-10', '11-20', '21-30', '31-40', '41-49'];
|
||
$zoneCountAll = array_fill(0, 5, 0);
|
||
$zoneCount30 = array_fill(0, 5, 0);
|
||
$zoneCount10 = array_fill(0, 5, 0);
|
||
|
||
foreach ($allHistory as $row) {
|
||
$num = (int)$row['num7'];
|
||
$zone = $this->_getZoneIdx($num);
|
||
if ($zone >= 0) $zoneCountAll[$zone]++;
|
||
}
|
||
foreach ($recent30 as $row) {
|
||
$num = (int)$row['num7'];
|
||
$zone = $this->_getZoneIdx($num);
|
||
if ($zone >= 0) $zoneCount30[$zone]++;
|
||
}
|
||
foreach ($recent10 as $row) {
|
||
$num = (int)$row['num7'];
|
||
$zone = $this->_getZoneIdx($num);
|
||
if ($zone >= 0) $zoneCount10[$zone]++;
|
||
}
|
||
|
||
// ====== 4. 波色分布分析 ======
|
||
$colorCountAll = ['红' => 0, '蓝' => 0, '绿' => 0];
|
||
$colorCount30 = ['红' => 0, '蓝' => 0, '绿' => 0];
|
||
$colorCount10 = ['红' => 0, '蓝' => 0, '绿' => 0];
|
||
|
||
foreach ($allHistory as $row) {
|
||
$num = (int)$row['num7'];
|
||
$color = $colorMap[$num] ?? '';
|
||
if (strpos($color, '红') !== false) $colorCountAll['红']++;
|
||
elseif (strpos($color, '蓝') !== false) $colorCountAll['蓝']++;
|
||
elseif (strpos($color, '绿') !== false) $colorCountAll['绿']++;
|
||
}
|
||
foreach ($recent30 as $row) {
|
||
$num = (int)$row['num7'];
|
||
$color = $colorMap[$num] ?? '';
|
||
if (strpos($color, '红') !== false) $colorCount30['红']++;
|
||
elseif (strpos($color, '蓝') !== false) $colorCount30['蓝']++;
|
||
elseif (strpos($color, '绿') !== false) $colorCount30['绿']++;
|
||
}
|
||
foreach ($recent10 as $row) {
|
||
$num = (int)$row['num7'];
|
||
$color = $colorMap[$num] ?? '';
|
||
if (strpos($color, '红') !== false) $colorCount10['红']++;
|
||
elseif (strpos($color, '蓝') !== false) $colorCount10['蓝']++;
|
||
elseif (strpos($color, '绿') !== false) $colorCount10['绿']++;
|
||
}
|
||
|
||
// ====== 5. 计算综合得分 ======
|
||
$scores = [];
|
||
$analysis = [
|
||
'last_special' => $lastSpecial,
|
||
'last_expect' => $lastExpect,
|
||
'weights' => $weights,
|
||
'omit_stats' => $omitStats,
|
||
'expected_freq' => round($expectedFreq, 2),
|
||
'zone_expected' => count($allHistory) / 5,
|
||
'color_expected' => count($allHistory) / 3,
|
||
'target_expect' => $targetExpect
|
||
];
|
||
|
||
for ($num = 1; $num <= 49; $num++) {
|
||
$totalScore = 0;
|
||
$detail = [];
|
||
|
||
// === 遗漏回归得分 ===
|
||
// 遗漏值超过历史平均遗漏值时,回归概率更高
|
||
$omitScore = 0;
|
||
$currentOmit = $omitCount[$num];
|
||
$avgOmit = $omitStats['avg'];
|
||
$stdOmit = $omitStats['std'];
|
||
|
||
if ($avgOmit > 0 && $stdOmit > 0) {
|
||
// 使用正态分布模型计算回归概率
|
||
// 遗漏值越偏离均值,回归得分越高
|
||
$zScore = ($currentOmit - $avgOmit) / $stdOmit;
|
||
if ($zScore > 0) {
|
||
// 遗漏值高于均值,回归概率增加
|
||
$omitScore = min(100, $zScore * 30); // 最高100分
|
||
} else {
|
||
// 遗漏值低于均值,回归概率降低
|
||
$omitScore = max(0, 10 + $zScore * 5); // 最低0分
|
||
}
|
||
} else {
|
||
$zScore = 0;
|
||
$omitScore = 50;
|
||
}
|
||
$detail['omit'] = $currentOmit;
|
||
$detail['omit_zscore'] = round($zScore, 2);
|
||
$detail['omit_score'] = round($omitScore, 2);
|
||
$totalScore += $omitScore * $weights['omit_regression'];
|
||
|
||
// === 频率回归得分(已合并统计显著性) ===
|
||
// 近期频率低于期望频率时,回归概率更高
|
||
$freqScore = 0;
|
||
$freqRatio = $freqAll[$num] / $expectedFreq;
|
||
$freqRatio30 = $freq30[$num] / $expectedFreq30;
|
||
$freqRatio10 = $freq10[$num] / $expectedFreq10;
|
||
|
||
// 近期频率低于期望,预测回归
|
||
if ($freqRatio30 < 1) {
|
||
$freqScore = (1 - $freqRatio30) * 50; // 低于期望时加分
|
||
} else {
|
||
$freqScore = max(0, 20 - ($freqRatio30 - 1) * 20); // 高于期望时减分
|
||
}
|
||
|
||
// 结合10期频率的极端情况
|
||
if ($freq10[$num] === 0) {
|
||
$freqScore += 30; // 最近10期未出现,额外加分
|
||
}
|
||
|
||
// 合并统计显著性:频率偏离期望越大,回归信号越强
|
||
$chiSquare = 0;
|
||
if ($expectedFreq > 0) {
|
||
$chiSquare = pow($freqAll[$num] - $expectedFreq, 2) / $expectedFreq;
|
||
}
|
||
if ($freqAll[$num] < $expectedFreq) {
|
||
// 频率低于期望(可能回归),加分
|
||
$chiBonus = min(30, $chiSquare * 5);
|
||
$freqScore += $chiBonus;
|
||
}
|
||
|
||
$detail['freq_all'] = $freqAll[$num];
|
||
$detail['freq_30'] = $freq30[$num];
|
||
$detail['freq_10'] = $freq10[$num];
|
||
$detail['freq_ratio_30'] = round($freqRatio30, 2);
|
||
$detail['chi_square'] = round($chiSquare, 2);
|
||
$detail['freq_score'] = round($freqScore, 2);
|
||
$totalScore += $freqScore * $weights['freq_regression'];
|
||
|
||
// === 近期趋势得分 ===
|
||
// 分析最近5期号码的走势趋势
|
||
$trendScore = 0;
|
||
$recent5 = array_slice($allHistory, 0, 5);
|
||
$recent5Nums = [];
|
||
foreach ($recent5 as $row) {
|
||
$recent5Nums[] = (int)$row['num7'];
|
||
}
|
||
// 计算号码与最近5期号码的距离
|
||
$avgDistance = 0;
|
||
foreach ($recent5Nums as $rNum) {
|
||
$avgDistance += abs($num - $rNum);
|
||
}
|
||
$avgDistance = $avgDistance / 5;
|
||
// 梯度评分:距离适中(15-22)得分最高,越偏离越低
|
||
if ($avgDistance >= 15 && $avgDistance <= 22) {
|
||
$trendScore = 80; // 最佳距离区间
|
||
} elseif ($avgDistance >= 10 && $avgDistance < 15) {
|
||
$trendScore = 60 + ($avgDistance - 10) * 4; // 60-80
|
||
} elseif ($avgDistance > 22 && $avgDistance <= 30) {
|
||
$trendScore = 60 - ($avgDistance - 22) * 3.75; // 60-30
|
||
} elseif ($avgDistance < 10) {
|
||
$trendScore = 20 + $avgDistance * 4; // 20-60
|
||
} else {
|
||
$trendScore = 10; // 距离过远
|
||
}
|
||
$detail['avg_distance'] = round($avgDistance, 2);
|
||
$detail['trend_score'] = $trendScore;
|
||
$totalScore += $trendScore * $weights['recent_trend'];
|
||
|
||
// === 区域平衡得分 ===
|
||
// 计算号码所在区域的近期失衡程度
|
||
$zone = $this->_getZoneIdx($num);
|
||
$zoneExpected30 = 30 / 5;
|
||
$zoneRatio = $zoneCount30[$zone] / $zoneExpected30;
|
||
$zoneScore = 0;
|
||
if ($zoneRatio < 1) {
|
||
$zoneScore = (1 - $zoneRatio) * 40; // 区域出现不足,加分
|
||
} else {
|
||
$zoneScore = max(0, 20 - ($zoneRatio - 1) * 20);
|
||
}
|
||
$detail['zone'] = $zoneLabels[$zone];
|
||
$detail['zone_count_30'] = $zoneCount30[$zone];
|
||
$detail['zone_score'] = round($zoneScore, 2);
|
||
$totalScore += $zoneScore * $weights['zone_balance'];
|
||
|
||
// === 波色平衡得分 ===
|
||
// 波色只有3种颜色,期望频率更高(30/3=10),波动比区域更小
|
||
// 使用更敏感的系数:波色不足时加分更多
|
||
$color = $colorMap[$num] ?? '';
|
||
$colorKey = '';
|
||
if (strpos($color, '红') !== false) $colorKey = '红';
|
||
elseif (strpos($color, '蓝') !== false) $colorKey = '蓝';
|
||
elseif (strpos($color, '绿') !== false) $colorKey = '绿';
|
||
|
||
$colorScore = 0;
|
||
if ($colorKey && isset($colorCount30[$colorKey])) {
|
||
$colorExpected30 = 30 / 3;
|
||
$colorRatio = $colorCount30[$colorKey] / $colorExpected30;
|
||
if ($colorRatio < 1) {
|
||
// 波色出现不足,加分(系数50,比区域的40更敏感)
|
||
$colorScore = (1 - $colorRatio) * 50;
|
||
} else {
|
||
// 波色出现过多,减分(系数25,比区域的20更严格)
|
||
$colorScore = max(0, 25 - ($colorRatio - 1) * 25);
|
||
}
|
||
}
|
||
$detail['color'] = $colorKey;
|
||
$detail['color_count_30'] = $colorCount30[$colorKey] ?? 0;
|
||
$detail['color_score'] = round($colorScore, 2);
|
||
$totalScore += $colorScore * $weights['color_balance'];
|
||
|
||
// 统计显著性已合并到频率回归得分中,此处不再重复计算
|
||
|
||
$scores[$num] = [
|
||
'num' => $num,
|
||
'score' => round($totalScore, 2),
|
||
'color' => $colorMap[$num] ?? '',
|
||
'animal' => $animalMap[$num] ?? '',
|
||
'detail' => $detail
|
||
];
|
||
}
|
||
|
||
// 按得分降序排序
|
||
$predictions = array_values($scores);
|
||
usort($predictions, function ($a, $b) {
|
||
return $b['score'] - $a['score'];
|
||
});
|
||
|
||
// 返回Top 5预测
|
||
$predictions = array_slice($predictions, 0, 5);
|
||
|
||
// ====== 6. 历史回测验证 ======
|
||
// 跳过回测以避免递归(回测内部调用此方法时会传入true)
|
||
$backtest = $skipBacktest ? null : $this->_runBacktest($periods, $weights, $backtestCount, $cutoffTime);
|
||
|
||
// 计算命中情况
|
||
$hitInfo = null;
|
||
if ($actualResult) {
|
||
$hitRank = -1;
|
||
foreach ($predictions as $idx => $p) {
|
||
if ($p['num'] === $actualResult['num7']) {
|
||
$hitRank = $idx + 1;
|
||
break;
|
||
}
|
||
}
|
||
$hitInfo = [
|
||
'hit' => $hitRank > 0,
|
||
'rank' => $hitRank,
|
||
'actual_num' => $actualResult['num7'],
|
||
'actual_color' => $actualResult['color'],
|
||
'actual_animal' => $actualResult['animal'],
|
||
'actual_expect' => $actualResult['expect']
|
||
];
|
||
}
|
||
|
||
return [
|
||
'predictions' => $predictions,
|
||
'last_special' => $lastSpecial,
|
||
'last_expect' => $lastExpect,
|
||
'analysis' => $analysis,
|
||
'actual_result' => $actualResult,
|
||
'hit_info' => $hitInfo,
|
||
'backtest' => $backtest
|
||
];
|
||
}
|
||
|
||
/**
|
||
* 获取号码所在区域索引
|
||
* @param int $num 号码
|
||
* @return int 区域索引(0-4),-1表示无效
|
||
*/
|
||
private function _getZoneIdx($num)
|
||
{
|
||
if ($num < 1 || $num > 49) return -1;
|
||
if ($num <= 10) return 0;
|
||
if ($num <= 20) return 1;
|
||
if ($num <= 30) return 2;
|
||
if ($num <= 40) return 3;
|
||
return 4;
|
||
}
|
||
|
||
/**
|
||
* 计算标准差
|
||
* @param array $data 数据数组
|
||
* @return float 标准差
|
||
*/
|
||
private function _calcStdDev($data)
|
||
{
|
||
if (count($data) < 2) return 0;
|
||
$mean = array_sum($data) / count($data);
|
||
$sumSq = 0;
|
||
foreach ($data as $val) {
|
||
$sumSq += pow($val - $mean, 2);
|
||
}
|
||
return sqrt($sumSq / count($data));
|
||
}
|
||
|
||
/**
|
||
* 历史回测验证
|
||
* 使用历史数据验证预测算法的有效性
|
||
* @param int $periods 统计期数
|
||
* @param array $weights 权重配置
|
||
* @param int $testCount 回测次数
|
||
* @param string|null $cutoffTime 截止时间(基于目标期号),null表示使用最新数据
|
||
* @return array {hit_rate: float, avg_rank: float, details: []}
|
||
*/
|
||
private function _runBacktest($periods, $weights, $testCount = 50, $cutoffTime = null)
|
||
{
|
||
// 获取足够的历史数据进行回测
|
||
$query = $this->field('expect,num7,openTime');
|
||
if ($cutoffTime) {
|
||
$query->where('openTime', '<', $cutoffTime);
|
||
}
|
||
$totalHistory = $query->order('openTime', 'desc')
|
||
->limit($periods + $testCount + 20)
|
||
->select();
|
||
|
||
if (count($totalHistory) < $periods + $testCount) {
|
||
return ['hit_rate' => 0, 'avg_rank' => 0, 'details' => [], 'error' => '数据不足'];
|
||
}
|
||
|
||
$hits = 0;
|
||
$ranks = [];
|
||
$details = [];
|
||
|
||
// 对最近 testCount 期进行回测
|
||
for ($i = 0; $i < $testCount; $i++) {
|
||
$targetRow = $totalHistory[$i];
|
||
$targetExpect = (string)$targetRow['expect'];
|
||
$actualNum = (int)$targetRow['num7'];
|
||
|
||
// 使用改进版预测算法进行预测(跳过回测避免递归)
|
||
$predResult = $this->getPredictionV2($periods, $weights, $targetExpect, true);
|
||
|
||
if (isset($predResult['error']) || empty($predResult['predictions'])) {
|
||
continue;
|
||
}
|
||
|
||
// 检查命中情况
|
||
$rank = -1;
|
||
foreach ($predResult['predictions'] as $idx => $p) {
|
||
if ($p['num'] === $actualNum) {
|
||
$rank = $idx + 1;
|
||
break;
|
||
}
|
||
}
|
||
|
||
if ($rank > 0) {
|
||
$hits++;
|
||
$ranks[] = $rank;
|
||
}
|
||
|
||
// 记录所有回测的详细信息
|
||
$details[] = [
|
||
'expect' => $targetExpect,
|
||
'actual' => $actualNum,
|
||
'predictions' => array_column($predResult['predictions'], 'num'),
|
||
'hit' => $rank > 0,
|
||
'rank' => $rank
|
||
];
|
||
}
|
||
|
||
$hitRate = $testCount > 0 ? round($hits / $testCount * 100, 2) : 0;
|
||
$avgRank = count($ranks) > 0 ? round(array_sum($ranks) / count($ranks), 2) : 0;
|
||
|
||
return [
|
||
'hit_rate' => $hitRate,
|
||
'avg_rank' => $avgRank,
|
||
'total_tests' => $testCount,
|
||
'total_hits' => $hits,
|
||
'details' => $details
|
||
];
|
||
}
|
||
|
||
/**
|
||
* 特码预测算法 V3 - 多维度综合预测
|
||
* 新增维度:转移概率(马尔可夫链)、单双规律、大小规律、走势方向分析
|
||
*
|
||
* @param int $periods 统计期数(默认200)
|
||
* @param array $weights 权重配置(可选)
|
||
* @param string $targetExpect 目标期号(可选,用于验证历史预测成功率)
|
||
* @param bool $skipBacktest 是否跳过回测(内部调用时使用)
|
||
* @param int $backtestCount 回测期数(默认50)
|
||
* @return array {predictions: [], last_special: int, analysis: {}, actual_result: {}, hit_info: {}, backtest: {}}
|
||
*/
|
||
public function getPredictionV3($periods = 200, $weights = [], $targetExpect = '', $skipBacktest = false, $backtestCount = 50)
|
||
{
|
||
// 获取号码属性映射
|
||
$num_model = new Num();
|
||
$colorMap = $num_model->column('color', 'num');
|
||
$animalMap = $num_model->column('animal', 'num');
|
||
|
||
// 默认权重配置(V3优化版,新增组合特征维度)
|
||
$defaultWeights = [
|
||
'omit_regression' => 0.18, // 遗漏回归权重
|
||
'freq_regression' => 0.12, // 频率回归权重
|
||
'transition_prob' => 0.18, // 转移概率权重
|
||
'oddeven_balance' => 0.08, // 单双平衡权重
|
||
'bigsmall_balance' => 0.08, // 大小平衡权重
|
||
'trend_direction' => 0.14, // 走势方向权重
|
||
'zone_balance' => 0.04, // 区域平衡权重
|
||
'color_balance' => 0.04, // 波色平衡权重
|
||
'combination' => 0.10 // 组合特征权重(新增)
|
||
];
|
||
$weights = array_merge($defaultWeights, $weights);
|
||
|
||
// 确定预测基准期和目标期
|
||
$actualResult = null;
|
||
$lastSpecial = 0;
|
||
$lastExpect = '';
|
||
$cutoffTime = null;
|
||
$allHistory = [];
|
||
|
||
if ($targetExpect) {
|
||
$targetRow = $this->where('expect', $targetExpect)->find();
|
||
if (!$targetRow) {
|
||
return ['predictions' => [], 'error' => '期号不存在', 'target_expect' => $targetExpect];
|
||
}
|
||
$cutoffTime = $targetRow['openTime'];
|
||
$actualResult = [
|
||
'expect' => (string)$targetRow['expect'],
|
||
'num7' => (int)$targetRow['num7'],
|
||
'color' => $colorMap[$targetRow['num7']] ?? '',
|
||
'animal' => $animalMap[$targetRow['num7']] ?? '',
|
||
'openTime' => $targetRow['openTime']
|
||
];
|
||
$prevRow = $this->where('openTime', '<', $cutoffTime)->order('openTime', 'desc')->limit(1)->find();
|
||
if (!$prevRow) {
|
||
return ['predictions' => [], 'error' => '目标期号之前没有历史数据', 'target_expect' => $targetExpect];
|
||
}
|
||
$lastSpecial = (int)$prevRow['num7'];
|
||
$lastExpect = (string)$prevRow['expect'];
|
||
$allHistory = $this->field('expect,num7,openTime')
|
||
->where('openTime', '<', $cutoffTime)
|
||
->order('openTime', 'desc')
|
||
->limit($periods)
|
||
->select();
|
||
} else {
|
||
$latest = $this->field('expect,num7,openTime')->order('openTime', 'desc')->limit(1)->find();
|
||
if (!$latest) {
|
||
return ['predictions' => [], 'last_special' => 0, 'analysis' => []];
|
||
}
|
||
$lastSpecial = (int)$latest['num7'];
|
||
$lastExpect = (string)$latest['expect'];
|
||
$allHistory = $this->field('expect,num7,openTime')
|
||
->order('openTime', 'desc')
|
||
->limit($periods)
|
||
->select();
|
||
}
|
||
|
||
if (empty($allHistory) || count($allHistory) < 30) {
|
||
return ['predictions' => [], 'error' => '历史数据不足(至少需要30期)', 'last_special' => $lastSpecial];
|
||
}
|
||
|
||
// 反转为升序(从旧到新)
|
||
$historyAsc = array_reverse($allHistory);
|
||
|
||
// ====== 1. 遗漏值分析(优化版 O(n))======
|
||
$omitCount = array_fill(1, 49, count($allHistory));
|
||
$omitHistory = [];
|
||
$lastAppear = array_fill(1, 49, -1);
|
||
|
||
foreach ($historyAsc as $idx => $row) {
|
||
$num = (int)$row['num7'];
|
||
if ($num >= 1 && $num <= 49) {
|
||
// 记录该号码在开奖时的遗漏值
|
||
if ($lastAppear[$num] >= 0) {
|
||
$omitHistory[] = $idx - $lastAppear[$num];
|
||
} else {
|
||
$omitHistory[] = $idx + 1;
|
||
}
|
||
$lastAppear[$num] = $idx;
|
||
// 更新当前遗漏(从最新位置到现在的距离)
|
||
$omitCount[$num] = count($historyAsc) - 1 - $idx;
|
||
}
|
||
}
|
||
|
||
// 历史中未出现的号码,遗漏值设为总期数
|
||
foreach ($omitCount as $n => $v) {
|
||
if ($lastAppear[$n] < 0) {
|
||
$omitCount[$n] = count($allHistory);
|
||
}
|
||
}
|
||
|
||
$omitStats = ['avg' => 0, 'max' => 0, 'std' => 0];
|
||
if (count($omitHistory) > 0) {
|
||
$omitStats['avg'] = array_sum($omitHistory) / count($omitHistory);
|
||
$omitStats['max'] = max($omitHistory);
|
||
$omitStats['std'] = $this->_calcStdDev($omitHistory);
|
||
}
|
||
|
||
// ====== 2. 频率分析 ======
|
||
$freqAll = array_fill(1, 49, 0);
|
||
foreach ($allHistory as $row) {
|
||
$num = (int)$row['num7'];
|
||
if ($num >= 1 && $num <= 49) {
|
||
$freqAll[$num]++;
|
||
}
|
||
}
|
||
$expectedFreq = count($allHistory) / 49;
|
||
|
||
$recent30 = array_slice($allHistory, 0, 30);
|
||
$freq30 = array_fill(1, 49, 0);
|
||
foreach ($recent30 as $row) {
|
||
$num = (int)$row['num7'];
|
||
if ($num >= 1 && $num <= 49) {
|
||
$freq30[$num]++;
|
||
}
|
||
}
|
||
$expectedFreq30 = 30 / 49;
|
||
|
||
$recent10 = array_slice($allHistory, 0, 10);
|
||
$freq10 = array_fill(1, 49, 0);
|
||
foreach ($recent10 as $row) {
|
||
$num = (int)$row['num7'];
|
||
if ($num >= 1 && $num <= 49) {
|
||
$freq10[$num]++;
|
||
}
|
||
}
|
||
$expectedFreq10 = 10 / 49;
|
||
|
||
// ====== 3. 转移概率分析 ======
|
||
// 根据历史数据量决定使用一阶或二阶马尔可夫
|
||
// 阈值条件:总期数 >= 200 且 状态对观察次数充足(>=5次的比例>=30%)
|
||
$minPeriodsThreshold = 200; // 二阶马尔可夫最小历史期数阈值(从100提升到200)
|
||
$minStatePairCount = 5; // 状态对最小观察次数
|
||
$use2ndOrder = false;
|
||
$secondOrderAvailable = false;
|
||
|
||
// 获取一阶转移概率矩阵(始终计算,作为fallback)
|
||
$zoneTransition = $this->_getTransitionMatrix($allHistory, 'zone');
|
||
$tailTransition = $this->_getTransitionMatrix($allHistory, 'tail');
|
||
$headTransition = $this->_getTransitionMatrix($allHistory, 'head');
|
||
|
||
// 获取二阶转移概率矩阵(数据充足时)
|
||
$zoneTransition2nd = null;
|
||
$tailTransition2nd = null;
|
||
$headTransition2nd = null;
|
||
$prev2Zone = 0;
|
||
$prev2Tail = 0;
|
||
$prev2Head = 0;
|
||
|
||
if (count($allHistory) >= $minPeriodsThreshold && count($allHistory) >= 2) {
|
||
// 获取前两期号码属性
|
||
$prev2Special = (int)$allHistory[1]['num7'];
|
||
$prev2Zone = $this->_getZoneIdx($prev2Special);
|
||
$prev2Tail = $prev2Special % 10;
|
||
$prev2Head = $this->_getHeadIdx($prev2Special);
|
||
|
||
// 构建二阶转移矩阵
|
||
$zoneTransition2nd = $this->_getTransitionMatrix2ndOrder($allHistory, 'zone', $minStatePairCount);
|
||
$tailTransition2nd = $this->_getTransitionMatrix2ndOrder($allHistory, 'tail', $minStatePairCount);
|
||
$headTransition2nd = $this->_getTransitionMatrix2ndOrder($allHistory, 'head', $minStatePairCount);
|
||
|
||
// 检查状态对观察次数是否充足(至少30%的状态对有足够观察)
|
||
// tail类型状态空间最大(100),以tail为基准判断
|
||
if ($tailTransition2nd['total_pairs'] > 0) {
|
||
$sufficientRatio = $tailTransition2nd['sufficient_pairs'] / $tailTransition2nd['total_pairs'];
|
||
$secondOrderAvailable = $sufficientRatio >= 0.3; // 至少30%状态对观察>=5次
|
||
}
|
||
|
||
$use2ndOrder = $secondOrderAvailable;
|
||
}
|
||
|
||
// 上期号码的各类属性
|
||
$lastZone = $this->_getZoneIdx($lastSpecial);
|
||
$lastTail = $lastSpecial % 10;
|
||
$lastHead = $this->_getHeadIdx($lastSpecial);
|
||
|
||
// ====== 4. 单双规律分析(新增)======
|
||
$oddevenStats = $this->_analyzeOddEven($allHistory);
|
||
|
||
// ====== 5. 大小规律分析(新增)======
|
||
$bigsmallStats = $this->_analyzeBigSmall($allHistory);
|
||
|
||
// ====== 6. 走势方向分析(新增)======
|
||
$trendDirection = $this->_analyzeTrendDirection($allHistory);
|
||
|
||
// ====== 6.1 动态权重调整(根据走势特征)======
|
||
$weights = $this->_adjustWeightsDynamic($weights, $trendDirection, $omitStats, $oddevenStats, $bigsmallStats);
|
||
|
||
// ====== 7. 区域分布分析 ======
|
||
$zoneLabels = ['1-10', '11-20', '21-30', '31-40', '41-49'];
|
||
$zoneCount30 = array_fill(0, 5, 0);
|
||
foreach ($recent30 as $row) {
|
||
$num = (int)$row['num7'];
|
||
$zone = $this->_getZoneIdx($num);
|
||
if ($zone >= 0) $zoneCount30[$zone]++;
|
||
}
|
||
|
||
// ====== 8. 波色分布分析 ======
|
||
$colorCount30 = ['红' => 0, '蓝' => 0, '绿' => 0];
|
||
foreach ($recent30 as $row) {
|
||
$num = (int)$row['num7'];
|
||
$color = $colorMap[$num] ?? '';
|
||
if (strpos($color, '红') !== false) $colorCount30['红']++;
|
||
elseif (strpos($color, '蓝') !== false) $colorCount30['蓝']++;
|
||
elseif (strpos($color, '绿') !== false) $colorCount30['绿']++;
|
||
}
|
||
|
||
// ====== 预计算号码属性索引 ======
|
||
$zoneMap = [];
|
||
$colorKeyMap = [];
|
||
$tailMap = [];
|
||
$headMap = [];
|
||
$isOddMap = [];
|
||
$isBigMap = [];
|
||
|
||
for ($n = 1; $n <= 49; $n++) {
|
||
$zoneMap[$n] = $this->_getZoneIdx($n);
|
||
$tailMap[$n] = $n % 10;
|
||
$headMap[$n] = $this->_getHeadIdx($n);
|
||
$isOddMap[$n] = $n % 2 === 1;
|
||
$isBigMap[$n] = $n >= 25;
|
||
$color = $colorMap[$n] ?? '';
|
||
if (strpos($color, '红') !== false) $colorKeyMap[$n] = '红';
|
||
elseif (strpos($color, '蓝') !== false) $colorKeyMap[$n] = '蓝';
|
||
elseif (strpos($color, '绿') !== false) $colorKeyMap[$n] = '绿';
|
||
else $colorKeyMap[$n] = '';
|
||
}
|
||
|
||
// ====== 8.1 组合特征分析(新增)======
|
||
$comboStats = $this->_analyzeAttributeCombinations($allHistory, $colorMap, $isOddMap, $isBigMap);
|
||
|
||
// ====== 9. 计算综合得分 ======
|
||
$scores = [];
|
||
$analysis = [
|
||
'last_special' => $lastSpecial,
|
||
'last_expect' => $lastExpect,
|
||
'weights' => $weights,
|
||
'weights_adjusted' => true, // 标记权重已动态调整
|
||
'omit_stats' => $omitStats,
|
||
'oddeven_stats' => $oddevenStats,
|
||
'bigsmall_stats' => $bigsmallStats,
|
||
'trend_direction' => $trendDirection,
|
||
'transition_order' => $use2ndOrder ? 2 : 1, // 转移概率阶数
|
||
'transition_available' => $secondOrderAvailable, // 二阶是否可用
|
||
'history_count' => count($allHistory), // 历史期数
|
||
'min_periods_threshold' => $minPeriodsThreshold, // 二阶阈值
|
||
'last_zone' => $zoneLabels[$lastZone] ?? '',
|
||
'last_tail' => $lastTail,
|
||
'last_head' => $lastHead,
|
||
'target_expect' => $targetExpect,
|
||
'combo_stats' => [
|
||
'oddbig_pct' => $comboStats['oddbig_pct'],
|
||
'oddsmall_pct' => $comboStats['oddsmall_pct'],
|
||
'evenbig_pct' => $comboStats['evenbig_pct'],
|
||
'evensmall_pct' => $comboStats['evensmall_pct']
|
||
]
|
||
];
|
||
|
||
// 预计算最近5期号码(用于趋势距离)
|
||
$recent5Nums = [];
|
||
foreach (array_slice($allHistory, 0, 5) as $row) {
|
||
$recent5Nums[] = (int)$row['num7'];
|
||
}
|
||
|
||
for ($num = 1; $num <= 49; $num++) {
|
||
$totalScore = 0;
|
||
$detail = [];
|
||
|
||
// === 遗漏回归得分(使用经验分布版本)===
|
||
$omitScore = $this->_calcOmitScoreEmpirical($omitCount[$num], $omitHistory, $omitStats);
|
||
$detail['omit'] = $omitCount[$num];
|
||
$detail['omit_score'] = round($omitScore, 2);
|
||
$totalScore += $omitScore * $weights['omit_regression'];
|
||
|
||
// === 频率回归得分(使用高级版本)===
|
||
$freqScore = $this->_calcFreqScoreAdvanced($freqAll[$num], $freq30[$num], $freq10[$num], $expectedFreq, $expectedFreq30, $omitHistory, $num);
|
||
$detail['freq_all'] = $freqAll[$num];
|
||
$detail['freq_30'] = $freq30[$num];
|
||
$detail['freq_score'] = round($freqScore, 2);
|
||
$totalScore += $freqScore * $weights['freq_regression'];
|
||
|
||
// === 转移概率得分(根据阶数选择计算方法)===
|
||
if ($use2ndOrder && $zoneTransition2nd && $tailTransition2nd && $headTransition2nd) {
|
||
$transScore = $this->_calcTransitionScore2ndOrder(
|
||
$num, $lastZone, $prev2Zone, $lastTail, $prev2Tail, $lastHead, $prev2Head,
|
||
$zoneTransition2nd, $tailTransition2nd, $headTransition2nd,
|
||
$zoneMap, $tailMap, $headMap
|
||
);
|
||
$detail['trans_order'] = 2;
|
||
} else {
|
||
$transScore = $this->_calcTransitionScore(
|
||
$num, $lastZone, $lastTail, $lastHead,
|
||
$zoneTransition, $tailTransition, $headTransition,
|
||
$zoneMap, $tailMap, $headMap
|
||
);
|
||
$detail['trans_order'] = 1;
|
||
}
|
||
$detail['trans_score'] = round($transScore, 2);
|
||
$totalScore += $transScore * $weights['transition_prob'];
|
||
|
||
// === 单双平衡得分(新增)===
|
||
$oddevenScore = $this->_calcOddEvenScore($num, $oddevenStats, $isOddMap);
|
||
$detail['is_odd'] = $isOddMap[$num];
|
||
$detail['oddeven_score'] = round($oddevenScore, 2);
|
||
$totalScore += $oddevenScore * $weights['oddeven_balance'];
|
||
|
||
// === 大小平衡得分(新增)===
|
||
$bigsmallScore = $this->_calcBigSmallScore($num, $bigsmallStats, $isBigMap);
|
||
$detail['is_big'] = $isBigMap[$num];
|
||
$detail['bigsmall_score'] = round($bigsmallScore, 2);
|
||
$totalScore += $bigsmallScore * $weights['bigsmall_balance'];
|
||
|
||
// === 走势方向得分(新增)===
|
||
$trendScore = $this->_calcTrendDirectionScore($num, $trendDirection, $recent5Nums);
|
||
$detail['trend_score'] = round($trendScore, 2);
|
||
$totalScore += $trendScore * $weights['trend_direction'];
|
||
|
||
// === 区域平衡得分 ===
|
||
$zone = $zoneMap[$num];
|
||
$zoneExpected30 = 30 / 5;
|
||
$zoneRatio = $zoneCount30[$zone] / $zoneExpected30;
|
||
$zoneScore = $zoneRatio < 1 ? (1 - $zoneRatio) * 40 : max(0, 20 - ($zoneRatio - 1) * 20);
|
||
$detail['zone'] = $zoneLabels[$zone];
|
||
$detail['zone_score'] = round($zoneScore, 2);
|
||
$totalScore += $zoneScore * $weights['zone_balance'];
|
||
|
||
// === 波色平衡得分 ===
|
||
$colorKey = $colorKeyMap[$num];
|
||
$colorScore = 0;
|
||
if ($colorKey && isset($colorCount30[$colorKey])) {
|
||
$colorExpected30 = 30 / 3;
|
||
$colorRatio = $colorCount30[$colorKey] / $colorExpected30;
|
||
$colorScore = $colorRatio < 1 ? (1 - $colorRatio) * 50 : max(0, 25 - ($colorRatio - 1) * 25);
|
||
}
|
||
$detail['color'] = $colorKey;
|
||
$detail['color_score'] = round($colorScore, 2);
|
||
$totalScore += $colorScore * $weights['color_balance'];
|
||
|
||
// === 组合特征得分(新增)===
|
||
$recent3Nums = array_slice($recent5Nums, 0, 3);
|
||
$comboScore = $this->_calcCombinationScore($num, $comboStats, $lastSpecial, $recent3Nums, $colorMap, $isOddMap, $isBigMap);
|
||
$detail['combo_score'] = round($comboScore, 2);
|
||
$totalScore += $comboScore * $weights['combination'];
|
||
|
||
$scores[$num] = [
|
||
'num' => $num,
|
||
'score' => round($totalScore, 2),
|
||
'color' => $colorMap[$num] ?? '',
|
||
'animal' => $animalMap[$num] ?? '',
|
||
'detail' => $detail
|
||
];
|
||
}
|
||
|
||
// 按得分降序排序
|
||
$predictions = array_values($scores);
|
||
usort($predictions, function ($a, $b) {
|
||
return $b['score'] - $a['score'];
|
||
});
|
||
|
||
// 返回Top 5预测
|
||
$predictions = array_slice($predictions, 0, 5);
|
||
|
||
// ====== 10. 历史回测验证 ======
|
||
$backtest = $skipBacktest ? null : $this->_runBacktestV3($periods, $weights, $backtestCount, $cutoffTime);
|
||
|
||
// ====== 11. 置信度评估(新增)======
|
||
// 最小数据量阈值设为50期,不足时置信度基于估算
|
||
$minDataThreshold = 50;
|
||
$confidence = $this->_calculateConfidence($predictions, $backtest, null, $minDataThreshold);
|
||
|
||
// 计算命中情况
|
||
$hitInfo = null;
|
||
if ($actualResult) {
|
||
$hitRank = -1;
|
||
foreach ($predictions as $idx => $p) {
|
||
if ($p['num'] === $actualResult['num7']) {
|
||
$hitRank = $idx + 1;
|
||
break;
|
||
}
|
||
}
|
||
$hitInfo = [
|
||
'hit' => $hitRank > 0,
|
||
'rank' => $hitRank,
|
||
'actual_num' => $actualResult['num7'],
|
||
'actual_color' => $actualResult['color'],
|
||
'actual_animal' => $actualResult['animal'],
|
||
'actual_expect' => $actualResult['expect']
|
||
];
|
||
}
|
||
|
||
return [
|
||
'predictions' => $predictions,
|
||
'last_special' => $lastSpecial,
|
||
'last_expect' => $lastExpect,
|
||
'analysis' => $analysis,
|
||
'actual_result' => $actualResult,
|
||
'hit_info' => $hitInfo,
|
||
'backtest' => $backtest,
|
||
'confidence' => $confidence // 新增置信度字段
|
||
];
|
||
}
|
||
|
||
/**
|
||
* 获取号码首号索引
|
||
* @param int $num 号码
|
||
* @return int 首号索引(0-4),-1表示无效
|
||
*/
|
||
private function _getHeadIdx($num)
|
||
{
|
||
if ($num < 1 || $num > 49) return -1;
|
||
if ($num <= 9) return 0;
|
||
if ($num <= 19) return 1;
|
||
if ($num <= 29) return 2;
|
||
if ($num <= 39) return 3;
|
||
return 4;
|
||
}
|
||
|
||
/**
|
||
* 获取转移概率矩阵(内部方法,优化版)
|
||
* @param array $history 历史数据(降序,最新在前)
|
||
* @param string $type 类型:zone/tail/head
|
||
* @return array {matrix: [], row_totals: [], expected_prob: float}
|
||
*/
|
||
private function _getTransitionMatrix($history, $type)
|
||
{
|
||
$historyAsc = array_reverse($history);
|
||
$categories = [];
|
||
$getIdx = null;
|
||
|
||
switch ($type) {
|
||
case 'zone':
|
||
$categories = ['1-10', '11-20', '21-30', '31-40', '41-49'];
|
||
$numCategories = 5;
|
||
$getIdx = function ($num) {
|
||
if ($num <= 10) return 0;
|
||
if ($num <= 20) return 1;
|
||
if ($num <= 30) return 2;
|
||
if ($num <= 40) return 3;
|
||
return 4;
|
||
};
|
||
break;
|
||
case 'tail':
|
||
$categories = ['尾0', '尾1', '尾2', '尾3', '尾4', '尾5', '尾6', '尾7', '尾8', '尾9'];
|
||
$numCategories = 10;
|
||
$getIdx = function ($num) { return $num % 10; };
|
||
break;
|
||
case 'head':
|
||
$categories = ['首0', '首1', '首2', '首3', '首4'];
|
||
$numCategories = 5;
|
||
$getIdx = function ($num) {
|
||
if ($num <= 9) return 0;
|
||
if ($num <= 19) return 1;
|
||
if ($num <= 29) return 2;
|
||
if ($num <= 39) return 3;
|
||
return 4;
|
||
};
|
||
break;
|
||
}
|
||
|
||
$matrix = array_fill(0, $numCategories, array_fill(0, $numCategories, 0));
|
||
$rowTotals = array_fill(0, $numCategories, 0);
|
||
|
||
for ($i = 0; $i < count($historyAsc) - 1; $i++) {
|
||
$currentNum = (int)$historyAsc[$i]['num7'];
|
||
$nextNum = (int)$historyAsc[$i + 1]['num7'];
|
||
if ($currentNum < 1 || $currentNum > 49 || $nextNum < 1 || $nextNum > 49) continue;
|
||
|
||
$from = $getIdx($currentNum);
|
||
$to = $getIdx($nextNum);
|
||
$matrix[$from][$to]++;
|
||
$rowTotals[$from]++;
|
||
}
|
||
|
||
// 计算概率矩阵(使用拉普拉斯平滑处理,避免极端概率)
|
||
$probMatrix = array_fill(0, $numCategories, array_fill(0, $numCategories, 0));
|
||
for ($i = 0; $i < $numCategories; $i++) {
|
||
// 平滑处理:(count + 1) / (total + categories)
|
||
// 避免因数据不足导致概率为0或极端值
|
||
$smoothTotal = $rowTotals[$i] + $numCategories;
|
||
for ($j = 0; $j < $numCategories; $j++) {
|
||
$probMatrix[$i][$j] = ($matrix[$i][$j] + 1) / $smoothTotal;
|
||
}
|
||
}
|
||
|
||
return [
|
||
'matrix' => $matrix,
|
||
'prob_matrix' => $probMatrix,
|
||
'row_totals' => $rowTotals,
|
||
'expected_prob' => 1 / $numCategories,
|
||
'categories' => $categories
|
||
];
|
||
}
|
||
|
||
/**
|
||
* 构建二阶马尔可夫转移矩阵
|
||
* 考虑前两期状态联合决定当前转移概率
|
||
*
|
||
* 状态空间说明:
|
||
* - 一阶马尔可夫: N个状态 (zone:5, tail:10, head:5)
|
||
* - 二阶马尔可夫: N^2个状态对 (zone:25, tail:100, head:25)
|
||
* - 状态键格式: "prev1-prev2",如 "2-3" 表示前一期区域2、前两期区域3
|
||
*
|
||
* 数据量阈值说明:
|
||
* - 建议历史数据 >= 200期以获得稳定的二阶概率估计
|
||
* - 状态对观察次数 >= 5 才使用该状态对的二阶概率
|
||
* - 观察次数不足时返回 state_pair_insufficient 标志,供调用者回退一阶
|
||
*
|
||
* @param array $history 历史数据(降序,最新在前)
|
||
* @param string $type 类型:zone/tail/head
|
||
* @param int $minStatePairCount 状态对最小观察次数,默认5
|
||
* @return array {matrix: [], prob_matrix: [], state_totals: [], num_categories: int, sufficient_pairs: int, total_pairs: int, min_threshold: int}
|
||
*/
|
||
private function _getTransitionMatrix2ndOrder($history, $type, $minStatePairCount = 5)
|
||
{
|
||
// 升序排列(从旧到新)
|
||
$historyAsc = array_reverse($history);
|
||
|
||
// 确定类别数量和索引函数
|
||
switch ($type) {
|
||
case 'zone':
|
||
$numCategories = 5;
|
||
$getIdx = function ($num) {
|
||
if ($num <= 10) return 0;
|
||
if ($num <= 20) return 1;
|
||
if ($num <= 30) return 2;
|
||
if ($num <= 40) return 3;
|
||
return 4;
|
||
};
|
||
break;
|
||
case 'tail':
|
||
$numCategories = 10;
|
||
$getIdx = function ($num) { return $num % 10; };
|
||
break;
|
||
case 'head':
|
||
$numCategories = 5;
|
||
$getIdx = function ($num) {
|
||
if ($num <= 9) return 0;
|
||
if ($num <= 19) return 1;
|
||
if ($num <= 29) return 2;
|
||
if ($num <= 39) return 3;
|
||
return 4;
|
||
};
|
||
break;
|
||
default:
|
||
return [
|
||
'matrix' => [],
|
||
'prob_matrix' => [],
|
||
'state_totals' => [],
|
||
'num_categories' => 0,
|
||
'sufficient_pairs' => 0,
|
||
'total_pairs' => 0,
|
||
'min_threshold' => $minStatePairCount
|
||
];
|
||
}
|
||
|
||
// 状态空间: (prev1, prev2) -> current,共 numCategories^2 个前置状态
|
||
$matrix = [];
|
||
$stateTotals = [];
|
||
|
||
// 初始化矩阵结构
|
||
for ($i = 0; $i < $numCategories; $i++) {
|
||
for ($j = 0; $j < $numCategories; $j++) {
|
||
$stateKey = $i . '-' . $j;
|
||
$matrix[$stateKey] = array_fill(0, $numCategories, 0);
|
||
$stateTotals[$stateKey] = 0;
|
||
}
|
||
}
|
||
|
||
// 统计二阶转移
|
||
for ($i = 0; $i < count($historyAsc) - 2; $i++) {
|
||
$prev1 = $getIdx((int)$historyAsc[$i]['num7']);
|
||
$prev2 = $getIdx((int)$historyAsc[$i + 1]['num7']);
|
||
$current = $getIdx((int)$historyAsc[$i + 2]['num7']);
|
||
|
||
if ($prev1 < 0 || $prev2 < 0 || $current < 0) continue;
|
||
|
||
$stateKey = $prev1 . '-' . $prev2;
|
||
$matrix[$stateKey][$current]++;
|
||
$stateTotals[$stateKey]++;
|
||
}
|
||
|
||
// 统计充分观察的状态对数量(观察次数 >= minStatePairCount)
|
||
$sufficientPairs = 0;
|
||
$totalPairs = $numCategories * $numCategories;
|
||
foreach ($stateTotals as $stateKey => $count) {
|
||
if ($count >= $minStatePairCount) {
|
||
$sufficientPairs++;
|
||
}
|
||
}
|
||
|
||
// 拉普拉斯平滑处理
|
||
$probMatrix = [];
|
||
foreach ($matrix as $stateKey => $counts) {
|
||
$smoothTotal = $stateTotals[$stateKey] + $numCategories;
|
||
$probMatrix[$stateKey] = [];
|
||
for ($j = 0; $j < $numCategories; $j++) {
|
||
$probMatrix[$stateKey][$j] = ($counts[$j] + 1) / $smoothTotal;
|
||
}
|
||
}
|
||
|
||
return [
|
||
'matrix' => $matrix,
|
||
'prob_matrix' => $probMatrix,
|
||
'state_totals' => $stateTotals,
|
||
'num_categories' => $numCategories,
|
||
'sufficient_pairs' => $sufficientPairs,
|
||
'total_pairs' => $totalPairs,
|
||
'min_threshold' => $minStatePairCount
|
||
];
|
||
}
|
||
|
||
/**
|
||
* 分析单双规律
|
||
* @param array $history 历史数据(降序)
|
||
* @return array {odd_count: int, even_count: int, odd_pct: float, recent_odd_streak: int, recent_even_streak: int, history_streaks: []}
|
||
*/
|
||
private function _analyzeOddEven($history)
|
||
{
|
||
$oddCount = 0;
|
||
$evenCount = 0;
|
||
$streaks = []; // 记录连续单/双的次数
|
||
|
||
$currentStreak = 0;
|
||
$lastType = null;
|
||
|
||
foreach ($history as $row) {
|
||
$num = (int)$row['num7'];
|
||
$isOdd = $num % 2 === 1;
|
||
|
||
if ($isOdd) {
|
||
$oddCount++;
|
||
$type = 'odd';
|
||
} else {
|
||
$evenCount++;
|
||
$type = 'even';
|
||
}
|
||
|
||
// 记录连续性
|
||
if ($lastType === null || $lastType !== $type) {
|
||
if ($currentStreak > 0) {
|
||
$streaks[] = ['type' => $lastType, 'length' => $currentStreak];
|
||
}
|
||
$currentStreak = 1;
|
||
$lastType = $type;
|
||
} else {
|
||
$currentStreak++;
|
||
}
|
||
}
|
||
|
||
// 最后一段连续
|
||
if ($currentStreak > 0) {
|
||
$streaks[] = ['type' => $lastType, 'length' => $currentStreak];
|
||
}
|
||
|
||
// 分析连续规律:连续单/双后反转的概率
|
||
$reverseAfterOddStreak = 0;
|
||
$reverseAfterEvenStreak = 0;
|
||
$oddStreakCount = 0;
|
||
$evenStreakCount = 0;
|
||
|
||
for ($i = 0; $i < count($streaks) - 1; $i++) {
|
||
if ($streaks[$i]['type'] === 'odd') {
|
||
$oddStreakCount++;
|
||
if ($streaks[$i + 1]['type'] === 'even') {
|
||
$reverseAfterOddStreak++;
|
||
}
|
||
} else {
|
||
$evenStreakCount++;
|
||
if ($streaks[$i + 1]['type'] === 'odd') {
|
||
$reverseAfterEvenStreak++;
|
||
}
|
||
}
|
||
}
|
||
|
||
$total = $oddCount + $evenCount;
|
||
$recentStreak = $currentStreak;
|
||
$recentType = $lastType;
|
||
|
||
return [
|
||
'odd_count' => $oddCount,
|
||
'even_count' => $evenCount,
|
||
'odd_pct' => $total > 0 ? round($oddCount / $total * 100, 1) : 50,
|
||
'even_pct' => $total > 0 ? round($evenCount / $total * 100, 1) : 50,
|
||
'recent_streak' => $recentStreak,
|
||
'recent_type' => $recentType,
|
||
'reverse_odd_rate' => $oddStreakCount > 0 ? round($reverseAfterOddStreak / $oddStreakCount * 100, 1) : 50,
|
||
'reverse_even_rate' => $evenStreakCount > 0 ? round($reverseAfterEvenStreak / $evenStreakCount * 100, 1) : 50,
|
||
'avg_odd_streak' => $this->_calcAvgStreak($streaks, 'odd'),
|
||
'avg_even_streak' => $this->_calcAvgStreak($streaks, 'even')
|
||
];
|
||
}
|
||
|
||
/**
|
||
* 分析大小规律
|
||
* @param array $history 历史数据(降序)
|
||
* @return array 同单双分析结构
|
||
*/
|
||
private function _analyzeBigSmall($history)
|
||
{
|
||
$bigCount = 0;
|
||
$smallCount = 0;
|
||
$streaks = [];
|
||
|
||
$currentStreak = 0;
|
||
$lastType = null;
|
||
|
||
foreach ($history as $row) {
|
||
$num = (int)$row['num7'];
|
||
$isBig = $num >= 25;
|
||
|
||
if ($isBig) {
|
||
$bigCount++;
|
||
$type = 'big';
|
||
} else {
|
||
$smallCount++;
|
||
$type = 'small';
|
||
}
|
||
|
||
if ($lastType === null || $lastType !== $type) {
|
||
if ($currentStreak > 0) {
|
||
$streaks[] = ['type' => $lastType, 'length' => $currentStreak];
|
||
}
|
||
$currentStreak = 1;
|
||
$lastType = $type;
|
||
} else {
|
||
$currentStreak++;
|
||
}
|
||
}
|
||
|
||
if ($currentStreak > 0) {
|
||
$streaks[] = ['type' => $lastType, 'length' => $currentStreak];
|
||
}
|
||
|
||
// 分析反转概率
|
||
$reverseAfterBigStreak = 0;
|
||
$reverseAfterSmallStreak = 0;
|
||
$bigStreakCount = 0;
|
||
$smallStreakCount = 0;
|
||
|
||
for ($i = 0; $i < count($streaks) - 1; $i++) {
|
||
if ($streaks[$i]['type'] === 'big') {
|
||
$bigStreakCount++;
|
||
if ($streaks[$i + 1]['type'] === 'small') {
|
||
$reverseAfterBigStreak++;
|
||
}
|
||
} else {
|
||
$smallStreakCount++;
|
||
if ($streaks[$i + 1]['type'] === 'big') {
|
||
$reverseAfterSmallStreak++;
|
||
}
|
||
}
|
||
}
|
||
|
||
$total = $bigCount + $smallCount;
|
||
|
||
return [
|
||
'big_count' => $bigCount,
|
||
'small_count' => $smallCount,
|
||
'big_pct' => $total > 0 ? round($bigCount / $total * 100, 1) : 50,
|
||
'small_pct' => $total > 0 ? round($smallCount / $total * 100, 1) : 50,
|
||
'recent_streak' => $currentStreak,
|
||
'recent_type' => $lastType,
|
||
'reverse_big_rate' => $bigStreakCount > 0 ? round($reverseAfterBigStreak / $bigStreakCount * 100, 1) : 50,
|
||
'reverse_small_rate' => $smallStreakCount > 0 ? round($reverseAfterSmallStreak / $smallStreakCount * 100, 1) : 50,
|
||
'avg_big_streak' => $this->_calcAvgStreak($streaks, 'big'),
|
||
'avg_small_streak' => $this->_calcAvgStreak($streaks, 'small')
|
||
];
|
||
}
|
||
|
||
/**
|
||
* 计算某类型的平均连续长度
|
||
* @param array $streaks 连续记录
|
||
* @param string $type 类型
|
||
* @return float 平均长度
|
||
*/
|
||
private function _calcAvgStreak($streaks, $type)
|
||
{
|
||
$sum = 0;
|
||
$count = 0;
|
||
foreach ($streaks as $s) {
|
||
if ($s['type'] === $type) {
|
||
$sum += $s['length'];
|
||
$count++;
|
||
}
|
||
}
|
||
return $count > 0 ? round($sum / $count, 1) : 1;
|
||
}
|
||
|
||
/**
|
||
* 分析走势方向
|
||
* @param array $history 历史数据(降序)
|
||
* @return array {trend_type: string, trend_strength: float, recent_nums: [], direction_score: float}
|
||
*/
|
||
private function _analyzeTrendDirection($history)
|
||
{
|
||
$recent5 = array_slice($history, 0, 5);
|
||
$nums = [];
|
||
foreach ($recent5 as $row) {
|
||
$nums[] = (int)$row['num7'];
|
||
}
|
||
|
||
if (count($nums) < 3) {
|
||
return ['trend_type' => 'unknown', 'trend_strength' => 0, 'recent_nums' => $nums];
|
||
}
|
||
|
||
// 计算走势类型
|
||
$ascCount = 0;
|
||
$descCount = 0;
|
||
$jumpCount = 0;
|
||
|
||
for ($i = 0; $i < count($nums) - 1; $i++) {
|
||
$diff = $nums[$i] - $nums[$i + 1]; // 注意:history是降序,所以nums[0]是最新的
|
||
if ($diff > 5) $ascCount++;
|
||
elseif ($diff < -5) $descCount++;
|
||
else $jumpCount++;
|
||
}
|
||
|
||
// 判断主要趋势
|
||
$trendType = 'jump';
|
||
$trendStrength = 0;
|
||
|
||
if ($ascCount >= 2 && $ascCount > $descCount) {
|
||
$trendType = 'ascending'; // 号码在减小(因为history降序)
|
||
$trendStrength = $ascCount / (count($nums) - 1);
|
||
} elseif ($descCount >= 2 && $descCount > $ascCount) {
|
||
$trendType = 'descending'; // 号码在增大
|
||
$trendStrength = $descCount / (count($nums) - 1);
|
||
} else {
|
||
$trendType = 'jump';
|
||
$trendStrength = $jumpCount / (count($nums) - 1);
|
||
}
|
||
|
||
// 计算平均变化幅度
|
||
$avgChange = 0;
|
||
for ($i = 0; $i < count($nums) - 1; $i++) {
|
||
$avgChange += abs($nums[$i] - $nums[$i + 1]);
|
||
}
|
||
$avgChange = $avgChange / (count($nums) - 1);
|
||
|
||
return [
|
||
'trend_type' => $trendType,
|
||
'trend_strength' => round($trendStrength, 2),
|
||
'avg_change' => round($avgChange, 1),
|
||
'recent_nums' => $nums
|
||
];
|
||
}
|
||
|
||
/**
|
||
* 动态调整权重(根据当前走势特征)
|
||
* @param array $baseWeights 基础权重
|
||
* @param array $trendDirection 走势方向分析结果
|
||
* @param array $omitStats 遗漏统计
|
||
* @param array $oddevenStats 单双统计
|
||
* @param array $bigsmallStats 大小统计
|
||
* @return array 调整后的权重
|
||
*/
|
||
private function _adjustWeightsDynamic($baseWeights, $trendDirection, $omitStats, $oddevenStats, $bigsmallStats)
|
||
{
|
||
$weights = $baseWeights;
|
||
$adjustments = [];
|
||
|
||
// 1. 强趋势时提高趋势方向权重
|
||
if ($trendDirection['trend_strength'] >= 0.7) {
|
||
$adjustments['trend_direction'] = 0.5; // +50%
|
||
} elseif ($trendDirection['trend_strength'] >= 0.5) {
|
||
$adjustments['trend_direction'] = 0.3; // +30%
|
||
}
|
||
|
||
// 2. 跳跃趋势时提高转移概率权重
|
||
if ($trendDirection['trend_type'] === 'jump') {
|
||
$adjustments['transition_prob'] = 0.2; // +20%
|
||
}
|
||
|
||
// 3. 长遗漏时提高遗漏回归权重
|
||
$avgOmit = $omitStats['avg'];
|
||
$maxOmit = $omitStats['max'];
|
||
if ($maxOmit > $avgOmit * 2) {
|
||
$adjustments['omit_regression'] = 0.3; // +30%
|
||
}
|
||
|
||
// 4. 单双连续过长时提高单双权重
|
||
if ($oddevenStats['recent_streak'] >= $oddevenStats['avg_odd_streak'] * 1.5 ||
|
||
$oddevenStats['recent_streak'] >= $oddevenStats['avg_even_streak'] * 1.5) {
|
||
$adjustments['oddeven_balance'] = 0.25; // +25%
|
||
}
|
||
|
||
// 5. 大小连续过长时提高大小权重
|
||
if ($bigsmallStats['recent_streak'] >= $bigsmallStats['avg_big_streak'] * 1.5 ||
|
||
$bigsmallStats['recent_streak'] >= $bigsmallStats['avg_small_streak'] * 1.5) {
|
||
$adjustments['bigsmall_balance'] = 0.25; // +25%
|
||
}
|
||
|
||
// 6. 单双比例严重失衡时提高权重
|
||
$oddPct = $oddevenStats['odd_pct'];
|
||
if ($oddPct > 60 || $oddPct < 40) {
|
||
$adjustments['oddeven_balance'] = max($adjustments['oddeven_balance'] ?? 0, 0.35);
|
||
}
|
||
|
||
// 7. 大小比例严重失衡时提高权重
|
||
$bigPct = $bigsmallStats['big_pct'];
|
||
if ($bigPct > 60 || $bigPct < 40) {
|
||
$adjustments['bigsmall_balance'] = max($adjustments['bigsmall_balance'] ?? 0, 0.35);
|
||
}
|
||
|
||
// 应用调整
|
||
foreach ($adjustments as $key => $multiplier) {
|
||
if (isset($weights[$key])) {
|
||
$weights[$key] = $weights[$key] * (1 + $multiplier);
|
||
}
|
||
}
|
||
|
||
// 归一化权重(确保总和为1)
|
||
$total = array_sum($weights);
|
||
if ($total > 0) {
|
||
foreach ($weights as $key => $value) {
|
||
$weights[$key] = $value / $total;
|
||
}
|
||
}
|
||
|
||
return $weights;
|
||
}
|
||
|
||
/**
|
||
* 计算遗漏回归得分
|
||
* @param int $currentOmit 当前遗漏值
|
||
* @param array $omitStats 遗漏统计
|
||
* @return float 得分
|
||
*/
|
||
private function _calcOmitScore($currentOmit, $omitStats)
|
||
{
|
||
$avgOmit = $omitStats['avg'];
|
||
$stdOmit = $omitStats['std'];
|
||
|
||
if ($avgOmit <= 0 || $stdOmit <= 0) return 50;
|
||
|
||
$zScore = ($currentOmit - $avgOmit) / $stdOmit;
|
||
|
||
if ($zScore > 0) {
|
||
return min(100, $zScore * 30);
|
||
} else {
|
||
return max(0, 10 + $zScore * 5);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 计算遗漏回归得分(经验分布版本,基于历史数据而非正态假设)
|
||
* @param int $currentOmit 当前遗漏值
|
||
* @param array $omitHistory 历史遗漏值数组
|
||
* @param array $omitStats 遗漏统计(可选备用)
|
||
* @return float 得分
|
||
*/
|
||
private function _calcOmitScoreEmpirical($currentOmit, $omitHistory, $omitStats = [])
|
||
{
|
||
if (empty($omitHistory) || count($omitHistory) < 10) {
|
||
// 数据不足时回退到正态分布版本
|
||
return $this->_calcOmitScore($currentOmit, $omitStats);
|
||
}
|
||
|
||
// 构建经验累积分布函数(CDF)
|
||
$sortedOmits = $omitHistory;
|
||
sort($sortedOmits);
|
||
$n = count($sortedOmits);
|
||
|
||
// 计算当前遗漏的百分位排名
|
||
$rank = 0;
|
||
for ($i = 0; $i < $n; $i++) {
|
||
if ($sortedOmits[$i] <= $currentOmit) {
|
||
$rank++;
|
||
}
|
||
}
|
||
$percentile = $rank / $n; // 0-1之间
|
||
|
||
// 高百分位(长期遗漏)给予高分,预测回归概率高
|
||
// 使用非线性映射:75%分位开始显著增加得分
|
||
$score = 0;
|
||
if ($percentile >= 0.9) {
|
||
// 极端遗漏(90%+分位):回归概率极高
|
||
$score = 80 + ($percentile - 0.9) * 200; // 80-100
|
||
} elseif ($percentile >= 0.75) {
|
||
// 较长遗漏(75%-90%分位):回归概率较高
|
||
$score = 50 + ($percentile - 0.75) * 120; // 50-80
|
||
} elseif ($percentile >= 0.5) {
|
||
// 中等遗漏(50%-75%分位):回归概率中等
|
||
$score = 25 + ($percentile - 0.5) * 100; // 25-50
|
||
} elseif ($percentile >= 0.25) {
|
||
// 较短遗漏(25%-50%分位):回归概率较低
|
||
$score = 10 + ($percentile - 0.25) * 60; // 10-25
|
||
} else {
|
||
// 短遗漏(0%-25%分位):刚出现不久,回归概率低
|
||
$score = $percentile * 40; // 0-10
|
||
}
|
||
|
||
// 结合历史最大遗漏值进行额外评估
|
||
$maxOmit = max($sortedOmits);
|
||
if ($currentOmit >= $maxOmit * 0.8) {
|
||
// 接近历史最大遗漏,额外加分
|
||
$score += 15;
|
||
}
|
||
|
||
return min(100, max(0, round($score, 2)));
|
||
}
|
||
|
||
/**
|
||
* 计算频率回归得分
|
||
* @param int $freqAll 全历史频率
|
||
* @param int $freq30 近30期频率
|
||
* @param int $freq10 近10期频率
|
||
* @param float $expectedFreq 期望频率
|
||
* @param float $expectedFreq30 近30期期望频率
|
||
* @return float 得分
|
||
*/
|
||
private function _calcFreqScore($freqAll, $freq30, $freq10, $expectedFreq, $expectedFreq30)
|
||
{
|
||
$freqRatio30 = $freq30 / $expectedFreq30;
|
||
|
||
$score = 0;
|
||
if ($freqRatio30 < 1) {
|
||
$score = (1 - $freqRatio30) * 50;
|
||
} else {
|
||
$score = max(0, 20 - ($freqRatio30 - 1) * 20);
|
||
}
|
||
|
||
// 最近10期未出现额外加分
|
||
if ($freq10 === 0) {
|
||
$score += 30;
|
||
}
|
||
|
||
// 全历史频率低于期望
|
||
if ($freqAll < $expectedFreq) {
|
||
$chiSquare = pow($freqAll - $expectedFreq, 2) / $expectedFreq;
|
||
$score += min(30, $chiSquare * 5);
|
||
}
|
||
|
||
return $score;
|
||
}
|
||
|
||
/**
|
||
* 计算频率回归得分(高级版,基于历史回归间隔分布)
|
||
* @param int $freqAll 全历史频率
|
||
* @param int $freq30 近30期频率
|
||
* @param int $freq10 近10期频率
|
||
* @param float $expectedFreq 期望频率
|
||
* @param float $expectedFreq30 近30期期望频率
|
||
* @param array $omitHistory 历史遗漏值数组(用于分析回归间隔)
|
||
* @param int $num 当前号码(用于追踪特定号码的遗漏)
|
||
* @return float 得分
|
||
*/
|
||
private function _calcFreqScoreAdvanced($freqAll, $freq30, $freq10, $expectedFreq, $expectedFreq30, $omitHistory = [], $num = 0)
|
||
{
|
||
$score = 0;
|
||
|
||
// 基础频率回归得分(与原版相同)
|
||
$freqRatio30 = $freq30 / $expectedFreq30;
|
||
if ($freqRatio30 < 1) {
|
||
$score = (1 - $freqRatio30) * 50;
|
||
} else {
|
||
$score = max(0, 20 - ($freqRatio30 - 1) * 20);
|
||
}
|
||
|
||
// 全历史频率低于期望时的卡方加分
|
||
if ($freqAll < $expectedFreq) {
|
||
$chiSquare = pow($freqAll - $expectedFreq, 2) / $expectedFreq;
|
||
$score += min(30, $chiSquare * 5);
|
||
}
|
||
|
||
// 高级版:基于遗漏历史判断是否在合理回归窗口
|
||
if (!empty($omitHistory) && count($omitHistory) >= 10) {
|
||
// 分析遗漏分布,计算典型回归间隔
|
||
$sortedOmits = $omitHistory;
|
||
sort($sortedOmits);
|
||
$n = count($sortedOmits);
|
||
|
||
// 计算中位数和四分位数
|
||
$medianIdx = floor($n / 2);
|
||
$q75Idx = floor($n * 0.75);
|
||
$medianOmit = $sortedOmits[$medianIdx];
|
||
$q75Omit = $sortedOmits[$q75Idx];
|
||
|
||
// 当前遗漏值(从omitHistory最后一个元素获取)
|
||
$currentOmit = end($omitHistory);
|
||
|
||
// 判断是否在回归窗口内
|
||
// 回归窗口定义:遗漏值接近或超过历史中位数时,回归概率增加
|
||
if ($freq10 === 0) {
|
||
// 近10期未出现:判断是否在合理回归窗口
|
||
if ($currentOmit >= $medianOmit && $currentOmit <= $q75Omit * 1.5) {
|
||
// 在回归窗口内,适度加分
|
||
$score += 25;
|
||
} elseif ($currentOmit > $q75Omit * 1.5) {
|
||
// 超出典型回归窗口很久,回归概率极高
|
||
$score += 35;
|
||
} elseif ($currentOmit >= $medianOmit * 0.8) {
|
||
// 接近中位数,回归概率适中
|
||
$score += 15;
|
||
}
|
||
// 低于中位数时不加分(刚出现不久)
|
||
}
|
||
} else {
|
||
// 数据不足时使用简化逻辑
|
||
if ($freq10 === 0) {
|
||
// 近10期未出现,但使用更保守的加分
|
||
$score += 20;
|
||
}
|
||
}
|
||
|
||
return min(100, $score);
|
||
}
|
||
|
||
/**
|
||
* 计算转移概率得分
|
||
* @param int $num 待评分号码
|
||
* @param int $lastZone 上期区域
|
||
* @param int $lastTail 上期尾数
|
||
* @param int $lastHead 上期首号
|
||
* @param array $zoneTrans 区域转移矩阵
|
||
* @param array $tailTrans 尾数转移矩阵
|
||
* @param array $headTrans 首号转移矩阵
|
||
* @param array $zoneMap 号码区域映射
|
||
* @param array $tailMap 号码尾数映射
|
||
* @param array $headMap 号码首号映射
|
||
* @return float 得分
|
||
*/
|
||
private function _calcTransitionScore($num, $lastZone, $lastTail, $lastHead, $zoneTrans, $tailTrans, $headTrans, $zoneMap, $tailMap, $headMap)
|
||
{
|
||
$score = 0;
|
||
|
||
// 区域转移概率
|
||
if ($lastZone >= 0 && $lastZone < 5) {
|
||
$toZone = $zoneMap[$num];
|
||
$zoneProb = $zoneTrans['prob_matrix'][$lastZone][$toZone] ?? 0;
|
||
$expectedZoneProb = $zoneTrans['expected_prob'];
|
||
// 高于期望概率加分
|
||
if ($zoneProb > $expectedZoneProb) {
|
||
$score += ($zoneProb - $expectedZoneProb) * 100;
|
||
}
|
||
}
|
||
|
||
// 尾数转移概率
|
||
if ($lastTail >= 0 && $lastTail < 10) {
|
||
$toTail = $tailMap[$num];
|
||
$tailProb = $tailTrans['prob_matrix'][$lastTail][$toTail] ?? 0;
|
||
$expectedTailProb = $tailTrans['expected_prob'];
|
||
if ($tailProb > $expectedTailProb) {
|
||
$score += ($tailProb - $expectedTailProb) * 80;
|
||
}
|
||
}
|
||
|
||
// 首号转移概率
|
||
if ($lastHead >= 0 && $lastHead < 5) {
|
||
$toHead = $headMap[$num];
|
||
$headProb = $headTrans['prob_matrix'][$lastHead][$toHead] ?? 0;
|
||
$expectedHeadProb = $headTrans['expected_prob'];
|
||
if ($headProb > $expectedHeadProb) {
|
||
$score += ($headProb - $expectedHeadProb) * 60;
|
||
}
|
||
}
|
||
|
||
return min(100, $score);
|
||
}
|
||
|
||
/**
|
||
* 计算二阶转移概率得分
|
||
*
|
||
* 计算方法:
|
||
* - 综合区域、尾号、首号三个维度的二阶转移概率
|
||
* - 各维度权重: 区域40%、尾号35%、首号25%
|
||
* - 得分范围: 0-100
|
||
*
|
||
* @param int $num 当前号码
|
||
* @param int $prev1Zone 前一期区域索引
|
||
* @param int $prev2Zone 前两期区域索引
|
||
* @param int $prev1Tail 前一期尾号索引
|
||
* @param int $prev2Tail 前两期尾号索引
|
||
* @param int $prev1Head 前一期首号索引
|
||
* @param int $prev2Head 前两期首号索引
|
||
* @param array $zoneTrans2nd 二阶区域转移矩阵
|
||
* @param array $tailTrans2nd 二阶尾号转移矩阵
|
||
* @param array $headTrans2nd 二阶首号转移矩阵
|
||
* @param array $zoneMap 号码区域映射
|
||
* @param array $tailMap 号码尾号映射
|
||
* @param array $headMap 号码首号映射
|
||
* @return float 综合转移得分 (0-100)
|
||
*/
|
||
private function _calcTransitionScore2ndOrder(
|
||
$num,
|
||
$prev1Zone, $prev2Zone,
|
||
$prev1Tail, $prev2Tail,
|
||
$prev1Head, $prev2Head,
|
||
$zoneTrans2nd, $tailTrans2nd, $headTrans2nd,
|
||
$zoneMap, $tailMap, $headMap
|
||
)
|
||
{
|
||
$zone = $zoneMap[$num];
|
||
$tail = $tailMap[$num];
|
||
$head = $headMap[$num];
|
||
|
||
$score = 0;
|
||
|
||
// 区域二阶转移得分(权重40%)
|
||
$zoneStateKey = $prev1Zone . '-' . $prev2Zone;
|
||
if (isset($zoneTrans2nd['prob_matrix'][$zoneStateKey][$zone])) {
|
||
$prob = $zoneTrans2nd['prob_matrix'][$zoneStateKey][$zone];
|
||
$score += $prob * 40;
|
||
}
|
||
|
||
// 尾号二阶转移得分(权重35%)
|
||
$tailStateKey = $prev1Tail . '-' . $prev2Tail;
|
||
if (isset($tailTrans2nd['prob_matrix'][$tailStateKey][$tail])) {
|
||
$prob = $tailTrans2nd['prob_matrix'][$tailStateKey][$tail];
|
||
$score += $prob * 35;
|
||
}
|
||
|
||
// 首号二阶转移得分(权重25%)
|
||
$headStateKey = $prev1Head . '-' . $prev2Head;
|
||
if (isset($headTrans2nd['prob_matrix'][$headStateKey][$head])) {
|
||
$prob = $headTrans2nd['prob_matrix'][$headStateKey][$head];
|
||
$score += $prob * 25;
|
||
}
|
||
|
||
return round($score, 2);
|
||
}
|
||
|
||
/**
|
||
* 计算单双平衡得分
|
||
* @param int $num 待评分号码
|
||
* @param array $stats 单双统计
|
||
* @param array $isOddMap 号码单双映射
|
||
* @return float 得分
|
||
*/
|
||
private function _calcOddEvenScore($num, $stats, $isOddMap)
|
||
{
|
||
$isOdd = $isOddMap[$num];
|
||
$score = 0;
|
||
|
||
// 当前连续性
|
||
$recentStreak = $stats['recent_streak'];
|
||
$recentType = $stats['recent_type'];
|
||
$avgStreak = $isOdd ? $stats['avg_odd_streak'] : $stats['avg_even_streak'];
|
||
|
||
// 如果当前连续长度超过平均值,预测反转
|
||
if ($recentStreak >= $avgStreak && $recentStreak >= 2) {
|
||
// 当前连续的是单,预测反转到双
|
||
if ($recentType === 'odd' && !$isOdd) {
|
||
$score += 40 + ($recentStreak - $avgStreak) * 10;
|
||
}
|
||
// 当前连续的是双,预测反转到单
|
||
if ($recentType === 'even' && $isOdd) {
|
||
$score += 40 + ($recentStreak - $avgStreak) * 10;
|
||
}
|
||
}
|
||
|
||
// 比例失衡回归
|
||
$oddPct = $stats['odd_pct'];
|
||
if ($oddPct > 55 && !$isOdd) {
|
||
// 单号过多,预测双号回归
|
||
$score += ($oddPct - 50) * 2;
|
||
} elseif ($oddPct < 45 && $isOdd) {
|
||
// 单号过少,预测单号回归
|
||
$score += (50 - $oddPct) * 2;
|
||
}
|
||
|
||
return min(100, $score);
|
||
}
|
||
|
||
/**
|
||
* 计算大小平衡得分
|
||
* @param int $num 待评分号码
|
||
* @param array $stats 大小统计
|
||
* @param array $isBigMap 号码大小映射
|
||
* @return float 得分
|
||
*/
|
||
private function _calcBigSmallScore($num, $stats, $isBigMap)
|
||
{
|
||
$isBig = $isBigMap[$num];
|
||
$score = 0;
|
||
|
||
$recentStreak = $stats['recent_streak'];
|
||
$recentType = $stats['recent_type'];
|
||
$avgStreak = $isBig ? $stats['avg_big_streak'] : $stats['avg_small_streak'];
|
||
|
||
// 连续性反转预测
|
||
if ($recentStreak >= $avgStreak && $recentStreak >= 2) {
|
||
if ($recentType === 'big' && !$isBig) {
|
||
$score += 40 + ($recentStreak - $avgStreak) * 10;
|
||
}
|
||
if ($recentType === 'small' && $isBig) {
|
||
$score += 40 + ($recentStreak - $avgStreak) * 10;
|
||
}
|
||
}
|
||
|
||
// 比例失衡回归
|
||
$bigPct = $stats['big_pct'];
|
||
if ($bigPct > 55 && !$isBig) {
|
||
$score += ($bigPct - 50) * 2;
|
||
} elseif ($bigPct < 45 && $isBig) {
|
||
$score += (50 - $bigPct) * 2;
|
||
}
|
||
|
||
return min(100, $score);
|
||
}
|
||
|
||
/**
|
||
* 分析属性组合特征
|
||
* @param array $history 历史数据(降序)
|
||
* @param array $colorMap 波色映射
|
||
* @param array $isOddMap 单双映射
|
||
* @param array $isBigMap 大小映射
|
||
* @return array 组合统计结果
|
||
*/
|
||
private function _analyzeAttributeCombinations($history, $colorMap, $isOddMap, $isBigMap)
|
||
{
|
||
// 属性组合统计(单+大、单+小、双+大、双+小)
|
||
$oddbigCount = 0;
|
||
$oddsmallCount = 0;
|
||
$evenbigCount = 0;
|
||
$evensmallCount = 0;
|
||
|
||
// 波色组合统计
|
||
$colorOddCount = ['红' => 0, '蓝' => 0, '绿' => 0];
|
||
$colorEvenCount = ['红' => 0, '蓝' => 0, '绿' => 0];
|
||
$colorBigCount = ['红' => 0, '蓝' => 0, '绿' => 0];
|
||
$colorSmallCount = ['红' => 0, '蓝' => 0, '绿' => 0];
|
||
|
||
// 相邻号码共现统计(±2范围内)
|
||
$adjacentPairs = [];
|
||
|
||
// 前3期特征对下期影响统计
|
||
$patternInfluence = [];
|
||
|
||
$historyAsc = array_reverse($history);
|
||
$n = count($historyAsc);
|
||
|
||
for ($i = 0; $i < $n; $i++) {
|
||
$num = (int)$historyAsc[$i]['num7'];
|
||
if ($num < 1 || $num > 49) continue;
|
||
|
||
$isOdd = $isOddMap[$num];
|
||
$isBig = $isBigMap[$num];
|
||
$color = $colorMap[$num] ?? '';
|
||
$colorKey = '';
|
||
if (strpos($color, '红') !== false) $colorKey = '红';
|
||
elseif (strpos($color, '蓝') !== false) $colorKey = '蓝';
|
||
elseif (strpos($color, '绿') !== false) $colorKey = '绿';
|
||
|
||
// 属性组合统计
|
||
if ($isOdd && $isBig) $oddbigCount++;
|
||
elseif ($isOdd && !$isBig) $oddsmallCount++;
|
||
elseif (!$isOdd && $isBig) $evenbigCount++;
|
||
else $evensmallCount++;
|
||
|
||
// 波色组合统计
|
||
if ($colorKey) {
|
||
if ($isOdd) $colorOddCount[$colorKey]++;
|
||
else $colorEvenCount[$colorKey]++;
|
||
if ($isBig) $colorBigCount[$colorKey]++;
|
||
else $colorSmallCount[$colorKey]++;
|
||
}
|
||
|
||
// 相邻号码共现统计(与前一期号码的关系)
|
||
if ($i > 0) {
|
||
$prevNum = (int)$historyAsc[$i - 1]['num7'];
|
||
// 记录±2范围内的共现
|
||
if (abs($num - $prevNum) <= 2) {
|
||
$pairKey = min($num, $prevNum) . '-' . max($num, $prevNum);
|
||
$adjacentPairs[$pairKey] = ($adjacentPairs[$pairKey] ?? 0) + 1;
|
||
}
|
||
}
|
||
|
||
// 前3期特征对下期影响
|
||
if ($i >= 3 && $i < $n) {
|
||
// 获取前3期的特征组合
|
||
$prev3Pattern = '';
|
||
for ($j = $i - 3; $j < $i; $j++) {
|
||
$prevNum = (int)$historyAsc[$j]['num7'];
|
||
$prevOdd = $isOddMap[$prevNum] ? 'O' : 'E';
|
||
$prevBig = $isBigMap[$prevNum] ? 'B' : 'S';
|
||
$prev3Pattern .= $prevOdd . $prevBig;
|
||
}
|
||
// 下期号码的特征
|
||
$nextOdd = $isOdd ? 'O' : 'E';
|
||
$nextBig = $isBig ? 'B' : 'S';
|
||
$nextPattern = $nextOdd . $nextBig;
|
||
|
||
$patternKey = $prev3Pattern . '->' . $nextPattern;
|
||
$patternInfluence[$patternKey] = ($patternInfluence[$patternKey] ?? 0) + 1;
|
||
}
|
||
}
|
||
|
||
$total = $n;
|
||
return [
|
||
'oddbig_pct' => $total > 0 ? round($oddbigCount / $total * 100, 1) : 25,
|
||
'oddsmall_pct' => $total > 0 ? round($oddsmallCount / $total * 100, 1) : 25,
|
||
'evenbig_pct' => $total > 0 ? round($evenbigCount / $total * 100, 1) : 25,
|
||
'evensmall_pct' => $total > 0 ? round($evensmallCount / $total * 100, 1) : 25,
|
||
'color_odd' => $colorOddCount,
|
||
'color_even' => $colorEvenCount,
|
||
'color_big' => $colorBigCount,
|
||
'color_small' => $colorSmallCount,
|
||
'adjacent_pairs' => $adjacentPairs,
|
||
'pattern_influence' => $patternInfluence
|
||
];
|
||
}
|
||
|
||
/**
|
||
* 计算组合特征得分
|
||
* @param int $num 待评分号码
|
||
* @param array $comboStats 组合统计结果
|
||
* @param int $lastNum 上期号码
|
||
* @param array $recent3Nums 最近3期号码
|
||
* @param array $colorMap 波色映射
|
||
* @param array $isOddMap 单双映射
|
||
* @param array $isBigMap 大小映射
|
||
* @return float 得分
|
||
*/
|
||
private function _calcCombinationScore($num, $comboStats, $lastNum, $recent3Nums, $colorMap, $isOddMap, $isBigMap)
|
||
{
|
||
$score = 0;
|
||
|
||
$isOdd = $isOddMap[$num];
|
||
$isBig = $isBigMap[$num];
|
||
$color = $colorMap[$num] ?? '';
|
||
$colorKey = '';
|
||
if (strpos($color, '红') !== false) $colorKey = '红';
|
||
elseif (strpos($color, '蓝') !== false) $colorKey = '蓝';
|
||
elseif (strpos($color, '绿') !== false) $colorKey = '绿';
|
||
|
||
// 1. 属性组合平衡得分
|
||
// 期望每个组合约占25%
|
||
$expectedPct = 25;
|
||
if ($isOdd && $isBig) {
|
||
$pct = $comboStats['oddbig_pct'];
|
||
if ($pct < $expectedPct) {
|
||
$score += ($expectedPct - $pct) * 0.8;
|
||
}
|
||
} elseif ($isOdd && !$isBig) {
|
||
$pct = $comboStats['oddsmall_pct'];
|
||
if ($pct < $expectedPct) {
|
||
$score += ($expectedPct - $pct) * 0.8;
|
||
}
|
||
} elseif (!$isOdd && $isBig) {
|
||
$pct = $comboStats['evenbig_pct'];
|
||
if ($pct < $expectedPct) {
|
||
$score += ($expectedPct - $pct) * 0.8;
|
||
}
|
||
} else {
|
||
$pct = $comboStats['evensmall_pct'];
|
||
if ($pct < $expectedPct) {
|
||
$score += ($expectedPct - $pct) * 0.8;
|
||
}
|
||
}
|
||
|
||
// 2. 波色组合平衡得分
|
||
if ($colorKey) {
|
||
$colorOddTotal = array_sum($comboStats['color_odd']);
|
||
$colorEvenTotal = array_sum($comboStats['color_even']);
|
||
if ($isOdd && $colorOddTotal > 0) {
|
||
$colorPct = round($comboStats['color_odd'][$colorKey] / $colorOddTotal * 100, 1);
|
||
if ($colorPct < 33) {
|
||
$score += (33 - $colorPct) * 0.5;
|
||
}
|
||
} elseif (!$isOdd && $colorEvenTotal > 0) {
|
||
$colorPct = round($comboStats['color_even'][$colorKey] / $colorEvenTotal * 100, 1);
|
||
if ($colorPct < 33) {
|
||
$score += (33 - $colorPct) * 0.5;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 3. 相邻号码关联得分
|
||
if ($lastNum >= 1 && $lastNum <= 49) {
|
||
// 检查与上期号码是否在±2范围内
|
||
$distance = abs($num - $lastNum);
|
||
if ($distance <= 2) {
|
||
$pairKey = min($num, $lastNum) . '-' . max($num, $lastNum);
|
||
$pairCount = $comboStats['adjacent_pairs'][$pairKey] ?? 0;
|
||
// 历史上相邻共现次数多,加分
|
||
if ($pairCount >= 3) {
|
||
$score += min(20, $pairCount * 3);
|
||
}
|
||
}
|
||
// 检查与上期号码±1的号码的共现
|
||
for ($offset = -1; $offset <= 1; $offset++) {
|
||
if ($offset === 0) continue;
|
||
$adjacentNum = $lastNum + $offset;
|
||
if ($adjacentNum >= 1 && $adjacentNum <= 49) {
|
||
$pairKey = min($num, $adjacentNum) . '-' . max($num, $adjacentNum);
|
||
$pairCount = $comboStats['adjacent_pairs'][$pairKey] ?? 0;
|
||
if ($pairCount >= 2) {
|
||
$score += min(10, $pairCount * 2);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 4. 前3期特征对下期影响得分
|
||
if (count($recent3Nums) >= 3) {
|
||
// 构建前3期特征模式
|
||
$prev3Pattern = '';
|
||
for ($i = 0; $i < 3; $i++) {
|
||
$prevNum = $recent3Nums[$i];
|
||
$prevOdd = $isOddMap[$prevNum] ? 'O' : 'E';
|
||
$prevBig = $isBigMap[$prevNum] ? 'B' : 'S';
|
||
$prev3Pattern .= $prevOdd . $prevBig;
|
||
}
|
||
// 当前号码的特征
|
||
$nextPattern = ($isOdd ? 'O' : 'E') . ($isBig ? 'B' : 'S');
|
||
$patternKey = $prev3Pattern . '->' . $nextPattern;
|
||
|
||
// 检查历史中该模式的出现频率
|
||
$patternCount = $comboStats['pattern_influence'][$patternKey] ?? 0;
|
||
$totalPatterns = array_sum($comboStats['pattern_influence']);
|
||
if ($totalPatterns > 0 && $patternCount > 0) {
|
||
$patternProb = $patternCount / $totalPatterns;
|
||
// 高于平均概率加分
|
||
$avgProb = 1 / 16; // 16种可能的nextPattern
|
||
if ($patternProb > $avgProb) {
|
||
$score += min(25, ($patternProb - $avgProb) * 100);
|
||
}
|
||
}
|
||
}
|
||
|
||
return min(100, round($score, 2));
|
||
}
|
||
|
||
/**
|
||
* 计算走势方向得分(优化版:延续与反转互斥,基于趋势强度动态分配)
|
||
* @param int $num 待评分号码
|
||
* @param array $trendDir 走势方向分析结果
|
||
* @param array $recent5Nums 最近5期号码
|
||
* @return float 得分
|
||
*/
|
||
private function _calcTrendDirectionScore($num, $trendDir, $recent5Nums)
|
||
{
|
||
$score = 0;
|
||
$trendType = $trendDir['trend_type'];
|
||
$trendStrength = $trendDir['trend_strength'];
|
||
$avgChange = $trendDir['avg_change'];
|
||
|
||
// 最近一期号码
|
||
$lastNum = $recent5Nums[0];
|
||
|
||
// 计算号码与最近一期的距离
|
||
$distance = abs($num - $lastNum);
|
||
|
||
// 延续得分与反转得分互斥,根据趋势强度分配权重
|
||
// 强趋势:延续概率高,反转概率低
|
||
// 弱趋势:反转概率相对提高
|
||
$continuationWeight = $trendStrength; // 延续权重随趋势强度增加
|
||
$reversalWeight = max(0, 1 - $trendStrength) * 0.5; // 反转权重随趋势强度减少
|
||
|
||
if ($trendType === 'descending' && $trendStrength >= 0.4) {
|
||
// 下降趋势(号码增大方向)
|
||
if ($num > $lastNum) {
|
||
// 延续趋势得分
|
||
$score += 35 * $continuationWeight;
|
||
if ($distance <= $avgChange + 8) {
|
||
$score += 15 * $continuationWeight;
|
||
}
|
||
} else {
|
||
// 反转得分(仅在弱趋势时有效)
|
||
if ($reversalWeight > 0.1) {
|
||
$score += 20 * $reversalWeight;
|
||
}
|
||
}
|
||
} elseif ($trendType === 'ascending' && $trendStrength >= 0.4) {
|
||
// 上升趋势(号码减小方向)
|
||
if ($num < $lastNum) {
|
||
$score += 35 * $continuationWeight;
|
||
if ($distance <= $avgChange + 8) {
|
||
$score += 15 * $continuationWeight;
|
||
}
|
||
} else {
|
||
if ($reversalWeight > 0.1) {
|
||
$score += 20 * $reversalWeight;
|
||
}
|
||
}
|
||
} elseif ($trendType === 'jump') {
|
||
// 跳跃趋势:号码跳到另一区间
|
||
if ($distance >= 12 && $distance <= 28) {
|
||
$score += 45;
|
||
}
|
||
// 跳跃趋势中也考虑适度反转
|
||
if ($distance >= 5 && $distance <= 15) {
|
||
$score += 20;
|
||
}
|
||
}
|
||
|
||
// 超强趋势连续过长时的反转预警(额外加分,不与上述互斥)
|
||
// 检查最近3期是否都在同一方向
|
||
if ($trendStrength >= 0.75 && count($recent5Nums) >= 3) {
|
||
$sameDirectionCount = 0;
|
||
for ($i = 0; $i < 2; $i++) {
|
||
$diff = $recent5Nums[$i] - $recent5Nums[$i + 1];
|
||
if ($trendType === 'descending' && $diff > 5) $sameDirectionCount++;
|
||
if ($trendType === 'ascending' && $diff < -5) $sameDirectionCount++;
|
||
}
|
||
// 连续2期同方向后,反转概率略微增加
|
||
if ($sameDirectionCount >= 2) {
|
||
if ($trendType === 'descending' && $num < $lastNum && $distance >= 10) {
|
||
$score += 12;
|
||
} elseif ($trendType === 'ascending' && $num > $lastNum && $distance >= 10) {
|
||
$score += 12;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 距离适中得分(基于最近5期的平均距离)
|
||
$avgDist = 0;
|
||
foreach ($recent5Nums as $rNum) {
|
||
$avgDist += abs($num - $rNum);
|
||
}
|
||
$avgDist = $avgDist / 5;
|
||
|
||
if ($avgDist >= 15 && $avgDist <= 22) {
|
||
$score += 25;
|
||
} elseif ($avgDist >= 10 && $avgDist < 15) {
|
||
$score += 15;
|
||
}
|
||
|
||
return min(100, $score);
|
||
}
|
||
|
||
/**
|
||
* 历史回测验证 V3
|
||
* @param int $periods 统计期数
|
||
* @param array $weights 权重配置
|
||
* @param int $testCount 回测次数
|
||
* @param string|null $cutoffTime 截止时间
|
||
* @return array {hit_rate: float, avg_rank: float, details: []}
|
||
*/
|
||
private function _runBacktestV3($periods, $weights, $testCount = 50, $cutoffTime = null)
|
||
{
|
||
$query = $this->field('expect,num7,openTime');
|
||
if ($cutoffTime) {
|
||
$query->where('openTime', '<', $cutoffTime);
|
||
}
|
||
$totalHistory = $query->order('openTime', 'desc')
|
||
->limit($periods + $testCount + 20)
|
||
->select();
|
||
|
||
if (count($totalHistory) < $periods + $testCount) {
|
||
return ['hit_rate' => 0, 'avg_rank' => 0, 'details' => [], 'error' => '数据不足'];
|
||
}
|
||
|
||
$hits = 0;
|
||
$ranks = [];
|
||
$details = [];
|
||
|
||
for ($i = 0; $i < $testCount; $i++) {
|
||
$targetRow = $totalHistory[$i];
|
||
$targetExpect = (string)$targetRow['expect'];
|
||
$actualNum = (int)$targetRow['num7'];
|
||
|
||
$predResult = $this->getPredictionV3($periods, $weights, $targetExpect, true);
|
||
|
||
if (isset($predResult['error']) || empty($predResult['predictions'])) {
|
||
continue;
|
||
}
|
||
|
||
$rank = -1;
|
||
foreach ($predResult['predictions'] as $idx => $p) {
|
||
if ($p['num'] === $actualNum) {
|
||
$rank = $idx + 1;
|
||
break;
|
||
}
|
||
}
|
||
|
||
if ($rank > 0) {
|
||
$hits++;
|
||
$ranks[] = $rank;
|
||
}
|
||
|
||
$details[] = [
|
||
'expect' => $targetExpect,
|
||
'actual' => $actualNum,
|
||
'predictions' => array_column($predResult['predictions'], 'num'),
|
||
'hit' => $rank > 0,
|
||
'rank' => $rank
|
||
];
|
||
}
|
||
|
||
$hitRate = $testCount > 0 ? round($hits / $testCount * 100, 2) : 0;
|
||
$avgRank = count($ranks) > 0 ? round(array_sum($ranks) / count($ranks), 2) : 0;
|
||
|
||
// 计算新增指标(添加数据量检查)
|
||
$minDataThreshold = 50; // 置信度计算最小数据量阈值
|
||
|
||
// 如果测试数据不足,返回默认值并添加警告
|
||
if ($testCount < $minDataThreshold) {
|
||
$ndcg5 = 0;
|
||
$mrr = 0;
|
||
$hitDistribution = [
|
||
'rank_1' => 0,
|
||
'rank_2' => 0,
|
||
'rank_3' => 0,
|
||
'rank_4' => 0,
|
||
'rank_5' => 0
|
||
];
|
||
$dataWarning = '回测数据不足(' . $testCount . '期),建议至少50期以获得可靠指标';
|
||
} else {
|
||
$ndcg5 = $this->_calculateNDCG($details, 5);
|
||
$mrr = $this->_calculateMRR($details);
|
||
$hitDistribution = $this->_calculateHitDistribution($details);
|
||
$dataWarning = null;
|
||
}
|
||
|
||
$precision5 = $testCount > 0 ? round($hits / ($testCount * 5) * 100, 2) : 0;
|
||
|
||
return [
|
||
'hit_rate' => $hitRate,
|
||
'avg_rank' => $avgRank,
|
||
'total_tests' => $testCount,
|
||
'total_hits' => $hits,
|
||
'details' => $details,
|
||
// 新增排名质量指标
|
||
'ndcg_5' => $ndcg5,
|
||
'mrr' => $mrr,
|
||
'hit_distribution' => $hitDistribution,
|
||
'precision_5' => $precision5,
|
||
// 数据量警告(不足时提示)
|
||
'data_warning' => $dataWarning,
|
||
'data_sufficient' => $testCount >= $minDataThreshold
|
||
];
|
||
}
|
||
|
||
/**
|
||
* 计算 NDCG@K (Normalized Discounted Cumulative Gain)
|
||
*
|
||
* 公式说明:
|
||
* - DCG (Discounted Cumulative Gain) = Σ(rel_i / log2(rank_i + 1))
|
||
* 其中 rel_i = 1 (命中) 或 0 (未命中),rank_i 为预测排名位置
|
||
* - IDCG (Ideal DCG) = Σ(1 / log2(i + 1)) for i = 1..min(hits, K)
|
||
* 即理想情况下所有命中的号码都排在最前面的DCG值
|
||
* - NDCG = DCG / IDCG,范围 0-1,越接近1表示排名质量越好
|
||
*
|
||
* @param array $backtestDetails 回测详情数组,每项包含 {hit: bool, rank: int}
|
||
* @param int $K Top-K 参数,默认5,评估前K个预测位置的排名质量
|
||
* @return float NDCG值 (0-1范围),空数据时返回0
|
||
*/
|
||
private function _calculateNDCG($backtestDetails, $K = 5)
|
||
{
|
||
// 边缘情况处理:空预测或无效参数
|
||
if (empty($backtestDetails) || $K <= 0) {
|
||
return 0;
|
||
}
|
||
|
||
$dcg = 0;
|
||
$idcg = 0;
|
||
|
||
// 计算 DCG: 命中号码的排名折损累积值
|
||
foreach ($backtestDetails as $detail) {
|
||
if (!isset($detail['hit']) || !isset($detail['rank'])) {
|
||
continue; // 跳过无效数据
|
||
}
|
||
if ($detail['hit'] && $detail['rank'] > 0 && $detail['rank'] <= $K) {
|
||
// DCG公式: rel / log2(rank + 1),命中时 rel=1
|
||
$dcg += 1 / log($detail['rank'] + 1, 2);
|
||
}
|
||
}
|
||
|
||
// 计算 IDCG: 最理想情况下所有命中的 DCG(假设都排在第1位)
|
||
$hitCount = 0;
|
||
foreach ($backtestDetails as $detail) {
|
||
if (isset($detail['hit']) && $detail['hit']) {
|
||
$hitCount++;
|
||
}
|
||
}
|
||
|
||
for ($i = 1; $i <= min($hitCount, $K); $i++) {
|
||
$idcg += 1 / log($i + 1, 2);
|
||
}
|
||
|
||
// 返回标准化值,IDCG为0时返回0避免除零错误
|
||
return $idcg > 0 ? round($dcg / $idcg, 4) : 0;
|
||
}
|
||
|
||
/**
|
||
* 计算 MRR (Mean Reciprocal Rank)
|
||
* 平均倒数排名,关注命中号码的具体排名位置
|
||
*
|
||
* 公式说明:
|
||
* - MRR = Σ(1/rank_i) / N,其中 rank_i 为命中号码的排名,N 为测试总数
|
||
* - 未命中的测试项贡献 0 到倒数排名
|
||
* - MRR 范围 0-1,越接近1表示命中号码平均排名越靠前
|
||
*
|
||
* @param array $backtestDetails 回测详情数组,每项包含 {hit: bool, rank: int}
|
||
* @return float MRR值 (0-1范围),空数据时返回0
|
||
*/
|
||
private function _calculateMRR($backtestDetails)
|
||
{
|
||
// 边缘情况处理:空预测
|
||
if (empty($backtestDetails)) {
|
||
return 0;
|
||
}
|
||
|
||
$reciprocalRanks = [];
|
||
|
||
foreach ($backtestDetails as $detail) {
|
||
if (!isset($detail['hit']) || !isset($detail['rank'])) {
|
||
continue; // 跳过无效数据
|
||
}
|
||
if ($detail['hit'] && $detail['rank'] > 0) {
|
||
$reciprocalRanks[] = 1 / $detail['rank'];
|
||
} else {
|
||
$reciprocalRanks[] = 0; // 未命中记为0
|
||
}
|
||
}
|
||
|
||
return count($reciprocalRanks) > 0
|
||
? round(array_sum($reciprocalRanks) / count($reciprocalRanks), 4)
|
||
: 0;
|
||
}
|
||
|
||
/**
|
||
* 计算命中率分布
|
||
* 统计各排名位置(1-5)的命中次数分布
|
||
*
|
||
* 结构定义:
|
||
* - 返回格式: {rank_1: n, rank_2: n, rank_3: n, rank_4: n, rank_5: n}
|
||
* - rank_N 表示预测排名第N位的命中次数
|
||
* - 用于前端柱状图可视化展示
|
||
*
|
||
* @param array $backtestDetails 回测详情数组,每项包含 {hit: bool, rank: int}
|
||
* @return array 各排名(1-5)的命中次数统计,键名为 rank_1 到 rank_5
|
||
*/
|
||
private function _calculateHitDistribution($backtestDetails)
|
||
{
|
||
// 边缘情况处理:空预测返回全0分布
|
||
if (empty($backtestDetails)) {
|
||
return [
|
||
'rank_1' => 0,
|
||
'rank_2' => 0,
|
||
'rank_3' => 0,
|
||
'rank_4' => 0,
|
||
'rank_5' => 0
|
||
];
|
||
}
|
||
|
||
// 初始化分布数组,键名使用 rank_N 格式便于前端解析
|
||
$distribution = [
|
||
'rank_1' => 0,
|
||
'rank_2' => 0,
|
||
'rank_3' => 0,
|
||
'rank_4' => 0,
|
||
'rank_5' => 0
|
||
];
|
||
|
||
foreach ($backtestDetails as $detail) {
|
||
if (!isset($detail['hit']) || !isset($detail['rank'])) {
|
||
continue; // 跳过无效数据
|
||
}
|
||
if ($detail['hit'] && $detail['rank'] >= 1 && $detail['rank'] <= 5) {
|
||
$key = 'rank_' . $detail['rank'];
|
||
$distribution[$key]++;
|
||
}
|
||
}
|
||
|
||
return $distribution;
|
||
}
|
||
|
||
/**
|
||
* 计算预测置信度
|
||
*
|
||
* 置信度组成(三个维度加权平均):
|
||
* - 维度1: 历史排名命中率 (权重0.4) - 基于回测数据统计各排名位置的命中率
|
||
* - 维度2: 得分分布置信度 (权重0.3) - 当前号码得分与Top5得分范围的比例关系
|
||
* - 维度3: 得分集中度 (权重0.3) - Top5得分与平均得分的差距,差距越大置信度越高
|
||
*
|
||
* 加权公式:
|
||
* confidence = 0.4 * historical_hit_rate + 0.3 * score_distribution + 0.3 * score_concentration
|
||
*
|
||
* 阈值定义:
|
||
* - 高置信度: >= 70% (绿色展示)
|
||
* - 中置信度: 50-70% (橙色展示)
|
||
* - 低置信度: < 50% (红色展示)
|
||
*
|
||
* @param array $predictions 预测结果数组(Top5)
|
||
* @param array $backtest 回测结果
|
||
* @param array $scoresAll 所有号码得分详情(可选,用于集中度计算)
|
||
* @param int $minDataThreshold 最小数据量阈值,默认50期
|
||
* @return array {confidence_scores: [], overall_confidence: float, data_warning: string|null}
|
||
*/
|
||
private function _calculateConfidence($predictions, $backtest, $scoresAll = null, $minDataThreshold = 50)
|
||
{
|
||
// 数据量检查
|
||
$dataWarning = null;
|
||
$hasBacktest = $backtest && !empty($backtest['details']) && $backtest['total_tests'] > 0;
|
||
|
||
if (!$hasBacktest || $backtest['total_tests'] < $minDataThreshold) {
|
||
$dataWarning = '回测数据不足(' . ($backtest['total_tests'] ?? 0) . '期),置信度基于估算,建议至少50期';
|
||
}
|
||
|
||
$confidenceScores = [];
|
||
|
||
// 计算Top5平均得分(用于集中度计算)
|
||
$avgScore = 0;
|
||
if (!empty($predictions)) {
|
||
$totalScore = array_sum(array_column($predictions, 'score'));
|
||
$avgScore = $totalScore / count($predictions);
|
||
}
|
||
|
||
foreach ($predictions as $idx => $pred) {
|
||
$rank = $idx + 1;
|
||
$num = $pred['num'];
|
||
$score = $pred['score'];
|
||
|
||
// 维度1: 历史排名命中率 (权重0.4)
|
||
$rankHitRate = $this->_getHistoricalHitRateByRank($rank, $backtest);
|
||
|
||
// 维度2: 得分分布置信度 (权重0.3) - 得分比例
|
||
$scoreDistribution = $this->_getScoreDistributionConfidence($score, $predictions);
|
||
|
||
// 维度3: 得分集中度 (权重0.3) - Top得分与平均得分的差距比例
|
||
$scoreConcentration = $this->_getScoreConcentration($score, $avgScore, $predictions);
|
||
|
||
// 综合置信度(加权平均)
|
||
$overallConfidence = $rankHitRate * 0.4 + $scoreDistribution * 0.3 + $scoreConcentration * 0.3;
|
||
|
||
$confidenceScores[] = [
|
||
'num' => $num,
|
||
'rank' => $rank,
|
||
'confidence' => round($overallConfidence * 100, 1),
|
||
'rank_hit_rate' => round($rankHitRate * 100, 1),
|
||
'score_distribution' => round($scoreDistribution * 100, 1),
|
||
'score_concentration' => round($scoreConcentration * 100, 1)
|
||
];
|
||
}
|
||
|
||
// 整体置信度(Top5平均)
|
||
$overallConfidence = count($confidenceScores) > 0
|
||
? round(array_sum(array_column($confidenceScores, 'confidence')) / count($confidenceScores), 1)
|
||
: 0;
|
||
|
||
return [
|
||
'confidence_scores' => $confidenceScores,
|
||
'overall_confidence' => $overallConfidence,
|
||
'data_warning' => $dataWarning
|
||
];
|
||
}
|
||
|
||
/**
|
||
* 基于历史排名获取命中率
|
||
*
|
||
* 计算方法:
|
||
* - 有回测数据时: 统计各排名的历史命中次数 / 总测试次数
|
||
* - 无回测数据时: 根据排名估算,排名越靠前置信度越高
|
||
* 估算公式: 1 - (rank - 1) * 0.15,即第1名估算85%,第5名估算25%
|
||
*
|
||
* @param int $rank 排名位置 (1-5)
|
||
* @param array $backtest 回测结果
|
||
* @return float 该排名的历史命中率 (0-1)
|
||
*/
|
||
private function _getHistoricalHitRateByRank($rank, $backtest)
|
||
{
|
||
if (!$backtest || empty($backtest['details']) || $backtest['total_tests'] == 0) {
|
||
// 无回测数据时,根据排名估算(排名越靠前置信度越高)
|
||
// 估算公式: 1 - (rank - 1) * 0.15
|
||
// 第1名: 1.0, 第2名: 0.85, 第3名: 0.70, 第4名: 0.55, 第5名: 0.40
|
||
return max(0, 1 - ($rank - 1) * 0.15);
|
||
}
|
||
|
||
// 统计各排名的历史命中次数
|
||
$rankHits = array_fill(1, 5, 0);
|
||
foreach ($backtest['details'] as $detail) {
|
||
if ($detail['hit'] && $detail['rank'] >= 1 && $detail['rank'] <= 5) {
|
||
$rankHits[$detail['rank']]++;
|
||
}
|
||
}
|
||
|
||
$totalTests = $backtest['total_tests'];
|
||
return $totalTests > 0 ? $rankHits[$rank] / $totalTests : 0;
|
||
}
|
||
|
||
/**
|
||
* 计算得分分布置信度
|
||
*
|
||
* 计算方法:
|
||
* - 得分比例 = (score - bottomScore) / (topScore - bottomScore)
|
||
* - 得分越接近第一名,置信度越高
|
||
* - 所有得分相同时返回1
|
||
*
|
||
* @param float $score 当前号码得分
|
||
* @param array $predictions 所有预测结果
|
||
* @return float 得分置信度 (0-1)
|
||
*/
|
||
private function _getScoreDistributionConfidence($score, $predictions)
|
||
{
|
||
if (empty($predictions)) return 0;
|
||
|
||
$topScore = $predictions[0]['score'];
|
||
$bottomScore = end($predictions)['score'];
|
||
|
||
if ($topScore == $bottomScore) return 1; // 所有得分相同
|
||
|
||
// 得分比例:(score - bottom) / (top - bottom)
|
||
$ratio = ($score - $bottomScore) / ($topScore - $bottomScore);
|
||
return max(0, min(1, $ratio));
|
||
}
|
||
|
||
/**
|
||
* 计算得分集中度
|
||
*
|
||
* 计算方法:
|
||
* - 集中度 = (score - avgScore) / (topScore - avgScore) 如果 score > avgScore
|
||
* - 集中度 = 0 如果 score <= avgScore
|
||
* - Top得分与平均得分差距越大,集中度越高,表示预测结果区分度明显
|
||
*
|
||
* @param float $score 当前号码得分
|
||
* @param float $avgScore Top5平均得分
|
||
* @param array $predictions 所有预测结果
|
||
* @return float 集中度置信度 (0-1)
|
||
*/
|
||
private function _getScoreConcentration($score, $avgScore, $predictions)
|
||
{
|
||
if (empty($predictions)) return 0;
|
||
|
||
$topScore = $predictions[0]['score'];
|
||
|
||
// 如果得分低于平均,集中度为0
|
||
if ($score <= $avgScore) {
|
||
return 0;
|
||
}
|
||
|
||
// 如果Top得分等于平均,所有得分相同,集中度为0.5
|
||
if ($topScore == $avgScore) {
|
||
return $score == $topScore ? 0.5 : 0;
|
||
}
|
||
|
||
// 集中度 = (score - avg) / (top - avg)
|
||
$concentration = ($score - $avgScore) / ($topScore - $avgScore);
|
||
return max(0, min(1, $concentration));
|
||
}
|
||
|
||
/**
|
||
* 权重网格搜索优化
|
||
*
|
||
* 优化目标定义:
|
||
* - 综合评估得分 = hit_rate * 0.6 + ndcg_5 * 100 * 0.4
|
||
* - 命中率权重60%,NDCG权重40%
|
||
* - 返回综合得分最高的权重配置
|
||
*
|
||
* 5种预定义权重配置:
|
||
* - 配置1: 遗漏优先型 - omit_regression权重最高(0.25)
|
||
* - 配置2: 转移概率优先型 - transition_prob权重最高(0.25)
|
||
* - 配置3: 走势方向优先型 - trend_direction权重最高(0.25)
|
||
* - 配置4: 平衡型 - 各维度权重较均衡
|
||
* - 配置5: 组合特征优先型 - combination权重最高(0.20)
|
||
*
|
||
* @param int $periods 统计期数,范围50-500
|
||
* @param int $backtestCount 回测期数,范围10-100
|
||
* @param int $timeoutSeconds 超时限制秒数,默认60秒
|
||
* @return array {best_weights: [], best_hit_rate: float, best_ndcg: float, all_results: [], timed_out: bool}
|
||
*/
|
||
private function _optimizeWeightsGridSearch($periods = 200, $backtestCount = 50, $timeoutSeconds = 60)
|
||
{
|
||
// 超时保护:记录开始时间
|
||
$startTime = microtime(true);
|
||
$timedOut = false;
|
||
|
||
// 5种预定义权重配置(具体权重值明确)
|
||
$weightConfigs = [
|
||
// 配置1: 遗漏优先型 - 遗漏回归权重最高
|
||
[
|
||
'omit_regression' => 0.25, // 遗漏回归权重25%
|
||
'freq_regression' => 0.12, // 频率回归权重12%
|
||
'transition_prob' => 0.15, // 转移概率权重15%
|
||
'trend_direction' => 0.12, // 走势方向权重12%
|
||
'oddeven_balance' => 0.08, // 单双平衡权重8%
|
||
'bigsmall_balance' => 0.08, // 大小平衡权重8%
|
||
'zone_balance' => 0.05, // 区域平衡权重5%
|
||
'color_balance' => 0.05, // 波色平衡权重5%
|
||
'combination' => 0.10 // 组合特征权重10%
|
||
],
|
||
// 配置2: 转移概率优先型 - 转移概率权重最高
|
||
[
|
||
'omit_regression' => 0.15,
|
||
'freq_regression' => 0.10,
|
||
'transition_prob' => 0.25, // 转移概率权重25%(最高)
|
||
'trend_direction' => 0.12,
|
||
'oddeven_balance' => 0.08,
|
||
'bigsmall_balance' => 0.08,
|
||
'zone_balance' => 0.04,
|
||
'color_balance' => 0.04,
|
||
'combination' => 0.14
|
||
],
|
||
// 配置3: 走势方向优先型 - 走势方向权重最高
|
||
[
|
||
'omit_regression' => 0.12,
|
||
'freq_regression' => 0.10,
|
||
'transition_prob' => 0.15,
|
||
'trend_direction' => 0.25, // 走势方向权重25%(最高)
|
||
'oddeven_balance' => 0.08,
|
||
'bigsmall_balance' => 0.08,
|
||
'zone_balance' => 0.04,
|
||
'color_balance' => 0.04,
|
||
'combination' => 0.12
|
||
],
|
||
// 配置4: 平衡型(默认配置)- 各维度权重较均衡
|
||
[
|
||
'omit_regression' => 0.18,
|
||
'freq_regression' => 0.12,
|
||
'transition_prob' => 0.18,
|
||
'trend_direction' => 0.14,
|
||
'oddeven_balance' => 0.08,
|
||
'bigsmall_balance' => 0.08,
|
||
'zone_balance' => 0.04,
|
||
'color_balance' => 0.04,
|
||
'combination' => 0.10
|
||
],
|
||
// 配置5: 组合特征优先型 - 组合特征权重最高
|
||
[
|
||
'omit_regression' => 0.15,
|
||
'freq_regression' => 0.10,
|
||
'transition_prob' => 0.15,
|
||
'trend_direction' => 0.12,
|
||
'oddeven_balance' => 0.06,
|
||
'bigsmall_balance' => 0.06,
|
||
'zone_balance' => 0.03,
|
||
'color_balance' => 0.03,
|
||
'combination' => 0.20 // 组合特征权重20%(最高)
|
||
]
|
||
];
|
||
|
||
$bestWeights = [];
|
||
$bestHitRate = 0;
|
||
$bestNdcg = 0;
|
||
$bestCombinedScore = 0;
|
||
$allResults = [];
|
||
|
||
// 执行每种配置的回测(添加超时检查)
|
||
foreach ($weightConfigs as $configIdx => $weights) {
|
||
// 超时检查:超过限制时间则停止
|
||
$elapsedTime = microtime(true) - $startTime;
|
||
if ($elapsedTime > $timeoutSeconds) {
|
||
$timedOut = true;
|
||
break;
|
||
}
|
||
|
||
// 执行回测
|
||
$backtest = $this->_runBacktestV3($periods, $weights, $backtestCount);
|
||
|
||
$hitRate = $backtest['hit_rate'] ?? 0;
|
||
$ndcg = $backtest['ndcg_5'] ?? 0;
|
||
$avgRank = $backtest['avg_rank'] ?? 0;
|
||
$mrr = $backtest['mrr'] ?? 0;
|
||
|
||
// 综合评估得分:命中率60% + NDCG40%
|
||
$combinedScore = $hitRate * 0.6 + $ndcg * 100 * 0.4;
|
||
|
||
$result = [
|
||
'config_name' => $configIdx + 1,
|
||
'config_type' => ['遗漏优先型', '转移概率优先型', '走势方向优先型', '平衡型', '组合特征优先型'][$configIdx],
|
||
'weights' => $weights,
|
||
'hit_rate' => $hitRate,
|
||
'avg_rank' => $avgRank,
|
||
'ndcg_5' => $ndcg,
|
||
'mrr' => $mrr,
|
||
'combined_score' => round($combinedScore, 2),
|
||
'total_hits' => $backtest['total_hits'] ?? 0
|
||
];
|
||
|
||
$allResults[] = $result;
|
||
|
||
// 更新最优配置
|
||
if ($combinedScore > $bestCombinedScore) {
|
||
$bestCombinedScore = $combinedScore;
|
||
$bestHitRate = $hitRate;
|
||
$bestNdcg = $ndcg;
|
||
$bestWeights = $weights;
|
||
}
|
||
}
|
||
|
||
// 按综合得分降序排序结果
|
||
usort($allResults, function($a, $b) {
|
||
return $b['combined_score'] - $a['combined_score'];
|
||
});
|
||
|
||
return [
|
||
'best_weights' => $bestWeights,
|
||
'best_hit_rate' => $bestHitRate,
|
||
'best_ndcg' => $bestNdcg,
|
||
'best_combined_score' => round($bestCombinedScore, 2),
|
||
'all_results' => $allResults,
|
||
'periods' => $periods,
|
||
'backtest_count' => $backtestCount,
|
||
'timeout_seconds' => $timeoutSeconds,
|
||
'timed_out' => $timedOut,
|
||
'elapsed_time' => round(microtime(true) - $startTime, 2)
|
||
];
|
||
}
|
||
|
||
/**
|
||
* 基于正码关联规律的特码预测方法(修正版)
|
||
* 核心规律:上期正码 → 当期特码
|
||
* - 覆盖区间规律:91.44% 当期特码在上期正码覆盖的区间内
|
||
* - 正码±3距离:59.36% 当期特码与上期正码某号码距离≤3
|
||
* - 双波色预测:69.52% 当期特码波色在上期正码前2种主导波色内
|
||
* - 特码区间转移:77.54% 基于上期特码区间预测当期特码区间
|
||
* - 平均值±10:41.98% 当期特码在上期正码平均值±10范围
|
||
* - 尾数±2:50% 和值尾数与特码尾数差≤2
|
||
* @param int $periods 统计期数(用于验证历史命中率)
|
||
* @param string $targetExpect 目标期号(可选,用于回测验证)
|
||
* @return array {predictions: [], analysis: {}, hit_info: {}, backtest: {}}
|
||
*/
|
||
public function getPredictionByNormalRelation($periods = 100, $targetExpect = '')
|
||
{
|
||
$num_model = new Num();
|
||
$colorMap = $num_model->column('color', 'num');
|
||
$animalMap = $num_model->column('animal', 'num');
|
||
|
||
// 区间划分:大号(31-49)、中号(11-30)、小号(1-10)
|
||
$getBigZone = function ($num) {
|
||
if ($num <= 10) return 'small';
|
||
if ($num <= 30) return 'mid';
|
||
return 'big';
|
||
};
|
||
|
||
// 细区间划分:1-10, 11-20, 21-30, 31-40, 41-49
|
||
$getFineZone = function ($num) {
|
||
if ($num <= 10) return 0;
|
||
if ($num <= 20) return 1;
|
||
if ($num <= 30) return 2;
|
||
if ($num <= 40) return 3;
|
||
return 4;
|
||
};
|
||
|
||
// 确定预测基准
|
||
$actualResult = null;
|
||
$lastNormals = [];
|
||
$lastSpecial = 0;
|
||
$lastExpect = '';
|
||
$cutoffTime = null;
|
||
|
||
if ($targetExpect) {
|
||
$targetRow = $this->where('expect', $targetExpect)->find();
|
||
if (!$targetRow) {
|
||
return ['predictions' => [], 'error' => '期号不存在', 'target_expect' => $targetExpect];
|
||
}
|
||
$cutoffTime = $targetRow['openTime'];
|
||
$actualResult = [
|
||
'expect' => (string)$targetRow['expect'],
|
||
'num7' => (int)$targetRow['num7'],
|
||
'color' => $colorMap[$targetRow['num7']] ?? '',
|
||
'animal' => $animalMap[$targetRow['num7']] ?? '',
|
||
'bigZone' => $getBigZone($targetRow['num7']),
|
||
'openTime' => $targetRow['openTime']
|
||
];
|
||
// 获取上一期数据作为预测基准
|
||
$prevRow = $this->where('openTime', '<', $cutoffTime)->order('openTime', 'desc')->limit(1)->find();
|
||
if (!$prevRow) {
|
||
return ['predictions' => [], 'error' => '没有历史数据'];
|
||
}
|
||
for ($i = 1; $i <= 6; $i++) {
|
||
$lastNormals[] = (int)$prevRow['num' . $i];
|
||
}
|
||
$lastSpecial = (int)$prevRow['num7'];
|
||
$lastExpect = (string)$prevRow['expect'];
|
||
} else {
|
||
// 使用最新一期作为预测基准
|
||
$latest = $this->field('expect,num1,num2,num3,num4,num5,num6,num7,openTime')
|
||
->order('openTime', 'desc')->limit(1)->find();
|
||
if (!$latest) {
|
||
return ['predictions' => [], 'error' => '没有历史数据'];
|
||
}
|
||
for ($i = 1; $i <= 6; $i++) {
|
||
$lastNormals[] = (int)$latest['num' . $i];
|
||
}
|
||
$lastSpecial = (int)$latest['num7'];
|
||
$lastExpect = (string)$latest['expect'];
|
||
}
|
||
|
||
// 分析上期正码特征
|
||
$normalMin = min($lastNormals);
|
||
$normalMax = max($lastNormals);
|
||
$normalAvg = round(array_sum($lastNormals) / 6, 2);
|
||
$normalSum = array_sum($lastNormals);
|
||
|
||
// 统计上期正码波色分布(找出前2种主导波色)
|
||
$colorCounts = ['红' => 0, '蓝' => 0, '绿' => 0];
|
||
foreach ($lastNormals as $n) {
|
||
$color = $colorMap[$n] ?? '';
|
||
if (strpos($color, '红') !== false) $colorCounts['红']++;
|
||
elseif (strpos($color, '蓝') !== false) $colorCounts['蓝']++;
|
||
elseif (strpos($color, '绿') !== false) $colorCounts['绿']++;
|
||
}
|
||
// 按数量排序,取前2种
|
||
$sortedColors = [];
|
||
foreach ($colorCounts as $c => $cnt) {
|
||
$sortedColors[] = ['color' => $c, 'count' => $cnt];
|
||
}
|
||
usort($sortedColors, function ($a, $b) { return $b['count'] - $a['count']; });
|
||
$top2Colors = [$sortedColors[0]['color'], $sortedColors[1]['color']];
|
||
|
||
// 获取上期正码覆盖的细区间
|
||
$normalFineZones = [];
|
||
foreach ($lastNormals as $n) {
|
||
$zoneIdx = $getFineZone($n);
|
||
if (!in_array($zoneIdx, $normalFineZones)) {
|
||
$normalFineZones[] = $zoneIdx;
|
||
}
|
||
}
|
||
|
||
// 获取上期正码覆盖的大区间
|
||
$normalBigZones = [];
|
||
foreach ($lastNormals as $n) {
|
||
$bigZone = $getBigZone($n);
|
||
if (!in_array($bigZone, $normalBigZones)) {
|
||
$normalBigZones[] = $bigZone;
|
||
}
|
||
}
|
||
|
||
// 上期特码所在大区间
|
||
$lastSpecialBigZone = $getBigZone($lastSpecial);
|
||
|
||
// 特码区间转移概率矩阵(基于历史分析)
|
||
$zoneTransMatrix = [
|
||
'big' => ['big' => 35.77, 'mid' => 37.96, 'small' => 26.28],
|
||
'mid' => ['big' => 33.77, 'mid' => 43.51, 'small' => 22.73],
|
||
'small' => ['big' => 42.17, 'mid' => 42.17, 'small' => 15.66]
|
||
];
|
||
|
||
// 计算每个号码的预测评分
|
||
$predictions = [];
|
||
for ($num = 1; $num <= 49; $num++) {
|
||
$numColor = $colorMap[$num] ?? '';
|
||
$numBigZone = $getBigZone($num);
|
||
$numFineZone = $getFineZone($num);
|
||
|
||
// 规律1:覆盖区间规律(91.44%命中)- 细区间覆盖
|
||
$fineZoneCovered = in_array($numFineZone, $normalFineZones);
|
||
$zoneCoverScore = $fineZoneCovered ? 91 : 0;
|
||
|
||
// 规律2:正码±3距离(59.36%命中)
|
||
$minDistance = 49;
|
||
foreach ($lastNormals as $n) {
|
||
$dist = abs($num - $n);
|
||
if ($dist < $minDistance) $minDistance = $dist;
|
||
}
|
||
$distScore = $minDistance <= 3 ? 59 : ($minDistance <= 5 ? 40 : 0);
|
||
|
||
// 规律3:双波色预测(69.52%命中)
|
||
$colorInTop2 = false;
|
||
foreach ($top2Colors as $tc) {
|
||
if (strpos($numColor, $tc) !== false) {
|
||
$colorInTop2 = true;
|
||
break;
|
||
}
|
||
}
|
||
$colorScore = $colorInTop2 ? 69 : 0;
|
||
|
||
// 规律4:特码区间转移(77.54%命中)
|
||
$transProb = $zoneTransMatrix[$lastSpecialBigZone][$numBigZone] ?? 0;
|
||
// 取该区间最高转移概率的2个区间
|
||
$transProbs = $zoneTransMatrix[$lastSpecialBigZone];
|
||
$sortedTrans = [];
|
||
foreach ($transProbs as $z => $p) {
|
||
$sortedTrans[] = ['zone' => $z, 'prob' => $p];
|
||
}
|
||
usort($sortedTrans, function ($a, $b) { return $b['prob'] - $a['prob']; });
|
||
$top2TransZones = [$sortedTrans[0]['zone'], $sortedTrans[1]['zone']];
|
||
$transMatch = in_array($numBigZone, $top2TransZones);
|
||
$transScore = $transMatch ? 77 : 0;
|
||
|
||
// 规律5:平均值±10(41.98%命中)
|
||
$avgDiff = abs($num - $normalAvg);
|
||
$avgScore = $avgDiff <= 10 ? 42 : 0;
|
||
|
||
// 规律6:尾数±2(50%命中)
|
||
$numTail = $num % 10;
|
||
$sumTail = $normalSum % 10;
|
||
$tailDiff = abs($numTail - $sumTail);
|
||
$tailDiff = min($tailDiff, 10 - $tailDiff);
|
||
$tailScore = $tailDiff <= 2 ? 50 : ($tailDiff <= 3 ? 30 : 0);
|
||
|
||
// 综合评分(加权求和)
|
||
$totalScore = $zoneCoverScore * 0.30 // 覆盖区间权重最高
|
||
+ $transScore * 0.25 // 特码区间转移
|
||
+ $colorScore * 0.20 // 双波色
|
||
+ $distScore * 0.12 // 距离
|
||
+ $avgScore * 0.08 // 平均值
|
||
+ $tailScore * 0.05; // 尾数
|
||
|
||
$predictions[] = [
|
||
'num' => $num,
|
||
'score' => round($totalScore, 2),
|
||
'color' => $numColor,
|
||
'animal' => $animalMap[$num] ?? '',
|
||
'big_zone' => $numBigZone,
|
||
'fine_zone' => $numFineZone,
|
||
'zone_covered' => $fineZoneCovered,
|
||
'min_distance' => $minDistance,
|
||
'color_in_top2' => $colorInTop2,
|
||
'trans_match' => $transMatch,
|
||
'trans_prob' => $transProb,
|
||
'avg_diff' => round($avgDiff, 2),
|
||
'tail_diff' => $tailDiff
|
||
];
|
||
}
|
||
|
||
// 按评分降序排序
|
||
usort($predictions, function ($a, $b) {
|
||
return $b['score'] - $a['score'];
|
||
});
|
||
|
||
// 返回Top15推荐号码
|
||
$topPredictions = array_slice($predictions, 0, 15);
|
||
|
||
// 分析信息
|
||
$analysis = [
|
||
'last_expect' => $lastExpect,
|
||
'last_normals' => $lastNormals,
|
||
'last_special' => $lastSpecial,
|
||
'last_special_zone' => $lastSpecialBigZone,
|
||
'normal_min' => $normalMin,
|
||
'normal_max' => $normalMax,
|
||
'normal_avg' => $normalAvg,
|
||
'normal_sum' => $normalSum,
|
||
'top2_colors' => $top2Colors,
|
||
'color_counts' => $colorCounts,
|
||
'normal_fine_zones' => $normalFineZones,
|
||
'normal_big_zones' => $normalBigZones,
|
||
'zone_trans_matrix' => $zoneTransMatrix,
|
||
'rules' => [
|
||
['name' => '覆盖区间', 'rate' => '91.44%', 'desc' => '当期特码在上期正码覆盖的细区间内'],
|
||
['name' => '特码区间转移', 'rate' => '77.54%', 'desc' => '基于上期特码区间预测当期特码所在大区间'],
|
||
['name' => '双波色预测', 'rate' => '69.52%', 'desc' => '当期特码波色在上期正码前2种主导波色内'],
|
||
['name' => '正码±3距离', 'rate' => '59.36%', 'desc' => '当期特码与上期正码某号码距离≤3'],
|
||
['name' => '尾数±2', 'rate' => '50%', 'desc' => '上期正码和值尾数与当期特码尾数差≤2'],
|
||
['name' => '平均值±10', 'rate' => '41.98%', 'desc' => '当期特码在上期正码平均值±10范围']
|
||
],
|
||
'predict_next_expect' => $lastExpect ? (string)(intval($lastExpect) + 1) : ''
|
||
];
|
||
|
||
// 命中验证
|
||
$hitInfo = null;
|
||
if ($actualResult) {
|
||
$hitRank = -1;
|
||
foreach ($topPredictions as $idx => $p) {
|
||
if ($p['num'] === $actualResult['num7']) {
|
||
$hitRank = $idx + 1;
|
||
break;
|
||
}
|
||
}
|
||
$fullRank = -1;
|
||
foreach ($predictions as $idx => $p) {
|
||
if ($p['num'] === $actualResult['num7']) {
|
||
$fullRank = $idx + 1;
|
||
break;
|
||
}
|
||
}
|
||
// 分析实际结果的规律命中情况
|
||
$actualAnalysis = null;
|
||
foreach ($predictions as $p) {
|
||
if ($p['num'] === $actualResult['num7']) {
|
||
$actualAnalysis = $p;
|
||
break;
|
||
}
|
||
}
|
||
$hitInfo = [
|
||
'hit' => $hitRank > 0,
|
||
'rank_in_top' => $hitRank,
|
||
'rank_in_all' => $fullRank,
|
||
'actual_num' => $actualResult['num7'],
|
||
'actual_color' => $actualResult['color'],
|
||
'actual_animal' => $actualResult['animal'],
|
||
'actual_expect' => $actualResult['expect'],
|
||
'actual_zone_covered' => $actualAnalysis ? $actualAnalysis['zone_covered'] : false,
|
||
'actual_min_distance' => $actualAnalysis ? $actualAnalysis['min_distance'] : 99,
|
||
'actual_color_in_top2' => $actualAnalysis ? $actualAnalysis['color_in_top2'] : false,
|
||
'actual_trans_match' => $actualAnalysis ? $actualAnalysis['trans_match'] : false,
|
||
'actual_tail_diff' => $actualAnalysis ? $actualAnalysis['tail_diff'] : 99,
|
||
'actual_avg_diff' => $actualAnalysis ? $actualAnalysis['avg_diff'] : 99
|
||
];
|
||
}
|
||
|
||
// 回测验证(默认显示前50期命中详情)
|
||
$backtest = null;
|
||
if ($periods >= 30) {
|
||
$backtest = $this->_runBacktestNormalRelation($periods, 50);
|
||
}
|
||
|
||
return [
|
||
'predictions' => $topPredictions,
|
||
'all_predictions' => $predictions,
|
||
'analysis' => $analysis,
|
||
'actual_result' => $actualResult,
|
||
'hit_info' => $hitInfo,
|
||
'backtest' => $backtest
|
||
];
|
||
}
|
||
|
||
/**
|
||
* 执行正码关联规律的历史回测(修正版)
|
||
* 使用正确规律:上期正码 → 当期特码
|
||
* @param int $periods 回测期数
|
||
* @param int $detailLimit 返回详情条数
|
||
* @return array {hit_rate, avg_rank, details, rule_stats}
|
||
*/
|
||
private function _runBacktestNormalRelation($periods = 100, $detailLimit = 20)
|
||
{
|
||
$history = $this->field('expect,num1,num2,num3,num4,num5,num6,num7,openTime')
|
||
->order('openTime', 'desc')
|
||
->limit($periods + 1)
|
||
->select();
|
||
|
||
if (count($history) < 2) {
|
||
return ['error' => '数据不足'];
|
||
}
|
||
|
||
$num_model = new Num();
|
||
$colorMap = $num_model->column('color', 'num');
|
||
|
||
// 大区间划分
|
||
$getBigZone = function ($num) {
|
||
if ($num <= 10) return 'small';
|
||
if ($num <= 30) return 'mid';
|
||
return 'big';
|
||
};
|
||
|
||
// 细区间划分
|
||
$getFineZone = function ($num) {
|
||
if ($num <= 10) return 0;
|
||
if ($num <= 20) return 1;
|
||
if ($num <= 30) return 2;
|
||
if ($num <= 40) return 3;
|
||
return 4;
|
||
};
|
||
|
||
// 特码区间转移概率矩阵
|
||
$zoneTransMatrix = [
|
||
'big' => ['big' => 35.77, 'mid' => 37.96, 'small' => 26.28],
|
||
'mid' => ['big' => 33.77, 'mid' => 43.51, 'small' => 22.73],
|
||
'small' => ['big' => 42.17, 'mid' => 42.17, 'small' => 15.66]
|
||
];
|
||
|
||
$hits = 0;
|
||
$ranks = [];
|
||
$details = [];
|
||
$ruleHits = [
|
||
'zone_cover' => 0,
|
||
'trans_match' => 0,
|
||
'color_top2' => 0,
|
||
'dist_3' => 0,
|
||
'tail_2' => 0,
|
||
'avg_10' => 0
|
||
];
|
||
|
||
for ($i = 0; $i < count($history) - 1; $i++) {
|
||
$currentRow = $history[$i];
|
||
$prevRow = $history[$i + 1];
|
||
|
||
// 使用上一期的正码预测当期的特码
|
||
$lastNormals = [];
|
||
for ($j = 1; $j <= 6; $j++) {
|
||
$lastNormals[] = (int)$prevRow['num' . $j];
|
||
}
|
||
$lastSpecial = (int)$prevRow['num7'];
|
||
$actualSpecial = (int)$currentRow['num7'];
|
||
|
||
// 分析上期正码特征
|
||
$normalMin = min($lastNormals);
|
||
$normalMax = max($lastNormals);
|
||
$normalAvg = array_sum($lastNormals) / 6;
|
||
$normalSum = array_sum($lastNormals);
|
||
|
||
// 波色分布
|
||
$colorCounts = ['红' => 0, '蓝' => 0, '绿' => 0];
|
||
foreach ($lastNormals as $n) {
|
||
$color = $colorMap[$n] ?? '';
|
||
if (strpos($color, '红') !== false) $colorCounts['红']++;
|
||
elseif (strpos($color, '蓝') !== false) $colorCounts['蓝']++;
|
||
elseif (strpos($color, '绿') !== false) $colorCounts['绿']++;
|
||
}
|
||
$sortedColors = [];
|
||
foreach ($colorCounts as $c => $cnt) {
|
||
$sortedColors[] = ['color' => $c, 'count' => $cnt];
|
||
}
|
||
usort($sortedColors, function ($a, $b) { return $b['count'] - $a['count']; });
|
||
$top2Colors = [$sortedColors[0]['color'], $sortedColors[1]['color']];
|
||
|
||
// 上期正码覆盖的细区间
|
||
$normalFineZones = [];
|
||
foreach ($lastNormals as $n) {
|
||
$zoneIdx = $getFineZone($n);
|
||
if (!in_array($zoneIdx, $normalFineZones)) {
|
||
$normalFineZones[] = $zoneIdx;
|
||
}
|
||
}
|
||
|
||
// 上期特码所在大区间
|
||
$lastSpecialBigZone = $getBigZone($lastSpecial);
|
||
|
||
// 计算每个号码评分
|
||
$scores = [];
|
||
for ($num = 1; $num <= 49; $num++) {
|
||
$numColor = $colorMap[$num] ?? '';
|
||
$numBigZone = $getBigZone($num);
|
||
$numFineZone = $getFineZone($num);
|
||
|
||
// 规律1:覆盖区间(91%)
|
||
$zoneCovered = in_array($numFineZone, $normalFineZones);
|
||
$zoneCoverScore = $zoneCovered ? 91 : 0;
|
||
|
||
// 规律2:特码区间转移(77%)
|
||
$transProbs = $zoneTransMatrix[$lastSpecialBigZone];
|
||
$sortedTrans = [];
|
||
foreach ($transProbs as $z => $p) {
|
||
$sortedTrans[] = ['zone' => $z, 'prob' => $p];
|
||
}
|
||
usort($sortedTrans, function ($a, $b) { return $b['prob'] - $a['prob']; });
|
||
$top2TransZones = [$sortedTrans[0]['zone'], $sortedTrans[1]['zone']];
|
||
$transMatch = in_array($numBigZone, $top2TransZones);
|
||
$transScore = $transMatch ? 77 : 0;
|
||
|
||
// 规律3:双波色(69%)
|
||
$colorInTop2 = false;
|
||
foreach ($top2Colors as $tc) {
|
||
if (strpos($numColor, $tc) !== false) {
|
||
$colorInTop2 = true;
|
||
break;
|
||
}
|
||
}
|
||
$colorScore = $colorInTop2 ? 69 : 0;
|
||
|
||
// 规律4:距离≤3(59%)
|
||
$minDist = 49;
|
||
foreach ($lastNormals as $n) {
|
||
$dist = abs($num - $n);
|
||
if ($dist < $minDist) $minDist = $dist;
|
||
}
|
||
$distScore = $minDist <= 3 ? 59 : ($minDist <= 5 ? 40 : 0);
|
||
|
||
// 规律5:尾数≤2(50%)
|
||
$tailDiff = abs($num % 10 - $normalSum % 10);
|
||
$tailDiff = min($tailDiff, 10 - $tailDiff);
|
||
$tailScore = $tailDiff <= 2 ? 50 : ($tailDiff <= 3 ? 30 : 0);
|
||
|
||
// 规律6:平均值±10(42%)
|
||
$avgDiff = abs($num - $normalAvg);
|
||
$avgScore = $avgDiff <= 10 ? 42 : 0;
|
||
|
||
// 综合评分
|
||
$score = $zoneCoverScore * 0.30
|
||
+ $transScore * 0.25
|
||
+ $colorScore * 0.20
|
||
+ $distScore * 0.12
|
||
+ $avgScore * 0.08
|
||
+ $tailScore * 0.05;
|
||
|
||
$scores[$num] = $score;
|
||
}
|
||
|
||
// 排序找排名
|
||
$sorted = [];
|
||
for ($num = 1; $num <= 49; $num++) {
|
||
$sorted[] = ['num' => $num, 'score' => $scores[$num]];
|
||
}
|
||
usort($sorted, function ($a, $b) {
|
||
return $b['score'] - $a['score'];
|
||
});
|
||
|
||
$rank = -1;
|
||
foreach ($sorted as $idx => $item) {
|
||
if ($item['num'] === $actualSpecial) {
|
||
$rank = $idx + 1;
|
||
break;
|
||
}
|
||
}
|
||
|
||
if ($rank > 0 && $rank <= 15) {
|
||
$hits++;
|
||
}
|
||
$ranks[] = $rank;
|
||
|
||
// 统计各规律命中情况
|
||
$actualFineZone = $getFineZone($actualSpecial);
|
||
$actualBigZone = $getBigZone($actualSpecial);
|
||
$actualColor = $colorMap[$actualSpecial] ?? '';
|
||
$actualMinDist = 49;
|
||
foreach ($lastNormals as $n) {
|
||
$dist = abs($actualSpecial - $n);
|
||
if ($dist < $actualMinDist) $actualMinDist = $dist;
|
||
}
|
||
$actualTailDiff = abs($actualSpecial % 10 - $normalSum % 10);
|
||
$actualTailDiff = min($actualTailDiff, 10 - $actualTailDiff);
|
||
$actualAvgDiff = abs($actualSpecial - $normalAvg);
|
||
|
||
if (in_array($actualFineZone, $normalFineZones)) $ruleHits['zone_cover']++;
|
||
if (in_array($actualBigZone, $top2TransZones)) $ruleHits['trans_match']++;
|
||
$actualColorInTop2 = false;
|
||
foreach ($top2Colors as $tc) {
|
||
if (strpos($actualColor, $tc) !== false) $actualColorInTop2 = true;
|
||
}
|
||
if ($actualColorInTop2) $ruleHits['color_top2']++;
|
||
if ($actualMinDist <= 3) $ruleHits['dist_3']++;
|
||
if ($actualTailDiff <= 2) $ruleHits['tail_2']++;
|
||
if ($actualAvgDiff <= 10) $ruleHits['avg_10']++;
|
||
|
||
$details[] = [
|
||
'expect' => (string)$currentRow['expect'],
|
||
'actual' => $actualSpecial,
|
||
'rank' => $rank,
|
||
'hit' => $rank > 0 && $rank <= 15
|
||
];
|
||
}
|
||
|
||
$totalPeriods = count($ranks);
|
||
$hitRate = $totalPeriods > 0 ? round($hits / $totalPeriods * 100, 2) : 0;
|
||
$avgRank = $totalPeriods > 0 ? round(array_sum($ranks) / $totalPeriods, 2) : 0;
|
||
|
||
// 计算各规律实际命中率
|
||
$ruleStats = [];
|
||
foreach ($ruleHits as $rule => $count) {
|
||
$ruleStats[$rule] = [
|
||
'hits' => $count,
|
||
'rate' => $totalPeriods > 0 ? round($count / $totalPeriods * 100, 2) : 0
|
||
];
|
||
}
|
||
|
||
return [
|
||
'periods' => $totalPeriods,
|
||
'hits' => $hits,
|
||
'hit_rate' => $hitRate,
|
||
'avg_rank' => $avgRank,
|
||
'rule_stats' => $ruleStats,
|
||
'details' => array_slice($details, 0, $detailLimit)
|
||
];
|
||
}
|
||
|
||
|
||
}
|