feat(11-05): 实现二阶马尔可夫转移概率增强
- 新增 _getTransitionMatrix2ndOrder 方法构建二阶转移矩阵 - 新增 _calcTransitionScore2ndOrder 方法计算二阶转移得分 - getPredictionV3 根据数据量和状态对观察次数自动选择阶数 - 阈值: 历史期数>=200,状态对观察>=5次,比例>=30% - analysis 返回新增 transition_order、transition_available 字段
This commit is contained in:
@@ -2227,12 +2227,49 @@ class History extends Model
|
|||||||
}
|
}
|
||||||
$expectedFreq10 = 10 / 49;
|
$expectedFreq10 = 10 / 49;
|
||||||
|
|
||||||
// ====== 3. 转移概率分析(新增)======
|
// ====== 3. 转移概率分析 ======
|
||||||
// 获取转移概率矩阵数据
|
// 根据历史数据量决定使用一阶或二阶马尔可夫
|
||||||
|
// 阈值条件:总期数 >= 200 且 状态对观察次数充足(>=5次的比例>=30%)
|
||||||
|
$minPeriodsThreshold = 200; // 二阶马尔可夫最小历史期数阈值(从100提升到200)
|
||||||
|
$minStatePairCount = 5; // 状态对最小观察次数
|
||||||
|
$use2ndOrder = false;
|
||||||
|
$secondOrderAvailable = false;
|
||||||
|
|
||||||
|
// 获取一阶转移概率矩阵(始终计算,作为fallback)
|
||||||
$zoneTransition = $this->_getTransitionMatrix($allHistory, 'zone');
|
$zoneTransition = $this->_getTransitionMatrix($allHistory, 'zone');
|
||||||
$tailTransition = $this->_getTransitionMatrix($allHistory, 'tail');
|
$tailTransition = $this->_getTransitionMatrix($allHistory, 'tail');
|
||||||
$headTransition = $this->_getTransitionMatrix($allHistory, 'head');
|
$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);
|
$lastZone = $this->_getZoneIdx($lastSpecial);
|
||||||
$lastTail = $lastSpecial % 10;
|
$lastTail = $lastSpecial % 10;
|
||||||
@@ -2304,6 +2341,10 @@ class History extends Model
|
|||||||
'oddeven_stats' => $oddevenStats,
|
'oddeven_stats' => $oddevenStats,
|
||||||
'bigsmall_stats' => $bigsmallStats,
|
'bigsmall_stats' => $bigsmallStats,
|
||||||
'trend_direction' => $trendDirection,
|
'trend_direction' => $trendDirection,
|
||||||
|
'transition_order' => $use2ndOrder ? 2 : 1, // 转移概率阶数
|
||||||
|
'transition_available' => $secondOrderAvailable, // 二阶是否可用
|
||||||
|
'history_count' => count($allHistory), // 历史期数
|
||||||
|
'min_periods_threshold' => $minPeriodsThreshold, // 二阶阈值
|
||||||
'last_zone' => $zoneLabels[$lastZone] ?? '',
|
'last_zone' => $zoneLabels[$lastZone] ?? '',
|
||||||
'last_tail' => $lastTail,
|
'last_tail' => $lastTail,
|
||||||
'last_head' => $lastHead,
|
'last_head' => $lastHead,
|
||||||
@@ -2339,12 +2380,22 @@ class History extends Model
|
|||||||
$detail['freq_score'] = round($freqScore, 2);
|
$detail['freq_score'] = round($freqScore, 2);
|
||||||
$totalScore += $freqScore * $weights['freq_regression'];
|
$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(
|
$transScore = $this->_calcTransitionScore(
|
||||||
$num, $lastZone, $lastTail, $lastHead,
|
$num, $lastZone, $lastTail, $lastHead,
|
||||||
$zoneTransition, $tailTransition, $headTransition,
|
$zoneTransition, $tailTransition, $headTransition,
|
||||||
$zoneMap, $tailMap, $headMap
|
$zoneMap, $tailMap, $headMap
|
||||||
);
|
);
|
||||||
|
$detail['trans_order'] = 1;
|
||||||
|
}
|
||||||
$detail['trans_score'] = round($transScore, 2);
|
$detail['trans_score'] = round($transScore, 2);
|
||||||
$totalScore += $transScore * $weights['transition_prob'];
|
$totalScore += $transScore * $weights['transition_prob'];
|
||||||
|
|
||||||
@@ -2541,6 +2592,124 @@ class History extends Model
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建二阶马尔可夫转移矩阵
|
||||||
|
* 考虑前两期状态联合决定当前转移概率
|
||||||
|
*
|
||||||
|
* 状态空间说明:
|
||||||
|
* - 一阶马尔可夫: 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 历史数据(降序)
|
* @param array $history 历史数据(降序)
|
||||||
@@ -3090,6 +3259,68 @@ class History extends Model
|
|||||||
return min(100, $score);
|
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 int $num 待评分号码
|
||||||
|
|||||||
Reference in New Issue
Block a user