<?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)
        ];
    }


}
