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;
|
||||
|
||||
// ====== 3. 转移概率分析(新增)======
|
||||
// 获取转移概率矩阵数据
|
||||
// ====== 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;
|
||||
@@ -2304,6 +2341,10 @@ class History extends Model
|
||||
'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,
|
||||
@@ -2339,12 +2380,22 @@ class History extends Model
|
||||
$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'];
|
||||
|
||||
@@ -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 历史数据(降序)
|
||||
@@ -3090,6 +3259,68 @@ class History extends Model
|
||||
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 待评分号码
|
||||
|
||||
Reference in New Issue
Block a user