智能选号功能丰富,加入多种模式
This commit is contained in:
parent
a093b50d5a
commit
3e07f72b98
212
README.md
212
README.md
@ -8,8 +8,11 @@
|
|||||||
- Python 3.8+
|
- Python 3.8+
|
||||||
- FastAPI
|
- FastAPI
|
||||||
- SQLAlchemy
|
- SQLAlchemy
|
||||||
- PostgreSQL
|
- MySQL/PostgreSQL
|
||||||
- Pydantic
|
- Pydantic
|
||||||
|
- Scikit-learn (机器学习)
|
||||||
|
- NumPy (数值计算)
|
||||||
|
- Pandas (数据处理)
|
||||||
|
|
||||||
### 前端
|
### 前端
|
||||||
- Vue 3
|
- Vue 3
|
||||||
@ -20,23 +23,42 @@
|
|||||||
- Vue Router
|
- Vue Router
|
||||||
|
|
||||||
## 系统功能
|
## 系统功能
|
||||||
1. 数据管理
|
|
||||||
- 双色球和大乐透历史数据导入
|
|
||||||
- 数据查询和筛选
|
|
||||||
- 数据导出
|
|
||||||
- 手动数据录入
|
|
||||||
|
|
||||||
2. 统计分析
|
### 1. 数据管理
|
||||||
- 号码出现频率统计
|
- 双色球和大乐透历史数据导入
|
||||||
- 热门号码分析
|
- 数据查询和筛选
|
||||||
- 冷门号码分析
|
- 数据导出
|
||||||
- 数据可视化展示
|
- 手动数据录入
|
||||||
|
- 自动数据更新
|
||||||
|
|
||||||
3. 智能选号
|
### 2. 基础统计分析
|
||||||
- 随机选号
|
- 号码出现频率统计
|
||||||
- 频率选号
|
- 热门号码分析
|
||||||
- 热门号码选号
|
- 冷门号码分析
|
||||||
- 冷门号码选号
|
- 数据可视化展示
|
||||||
|
|
||||||
|
### 3. 高级数据分析 ⭐ 新功能
|
||||||
|
- **遗漏值分析**: 分析各号码的遗漏期数
|
||||||
|
- **和值分析**: 统计红球和值的分布规律
|
||||||
|
- **AC值分析**: 邻号差值分析
|
||||||
|
- **质合比分析**: 质数与合数的比例分析
|
||||||
|
- **012路分析**: 除3余数分析
|
||||||
|
- **跨度分析**: 最大最小号码差值分析
|
||||||
|
- **综合分析**: 多维度数据整合分析
|
||||||
|
|
||||||
|
### 4. 智能预测系统 ⭐ 新功能
|
||||||
|
- **机器学习预测**: 基于历史数据的AI预测
|
||||||
|
- **模式预测**: 基于统计模式的预测
|
||||||
|
- **集成预测**: 多方法综合预测
|
||||||
|
- **预测模型训练**: 可自定义训练参数
|
||||||
|
- **预测结果评估**: 预测准确率统计
|
||||||
|
|
||||||
|
### 5. 智能选号
|
||||||
|
- 随机选号
|
||||||
|
- 频率选号
|
||||||
|
- 热门号码选号
|
||||||
|
- 冷门号码选号
|
||||||
|
- 自定义选号策略
|
||||||
|
|
||||||
## 项目结构
|
## 项目结构
|
||||||
```
|
```
|
||||||
@ -44,10 +66,15 @@ lottery/
|
|||||||
├── backend/ # 后端代码
|
├── backend/ # 后端代码
|
||||||
│ ├── app/
|
│ ├── app/
|
||||||
│ │ ├── api/ # API 路由
|
│ │ ├── api/ # API 路由
|
||||||
|
│ │ │ ├── endpoints/ # 基础API端点
|
||||||
|
│ │ │ └── v1/ # API版本1
|
||||||
│ │ ├── core/ # 核心配置
|
│ │ ├── core/ # 核心配置
|
||||||
│ │ ├── models/ # 数据模型
|
│ │ ├── models/ # 数据模型
|
||||||
│ │ ├── schemas/ # 数据验证
|
│ │ ├── schemas/ # 数据验证
|
||||||
│ │ └── services/ # 业务逻辑
|
│ │ └── services/ # 业务逻辑
|
||||||
|
│ │ ├── analysis_service.py # 基础分析服务
|
||||||
|
│ │ ├── advanced_analysis.py # 高级分析服务 ⭐
|
||||||
|
│ │ └── prediction_service.py # 预测服务 ⭐
|
||||||
│ ├── requirements.txt # 依赖包
|
│ ├── requirements.txt # 依赖包
|
||||||
│ └── main.py # 入口文件
|
│ └── main.py # 入口文件
|
||||||
├── frontend/ # 前端代码
|
├── frontend/ # 前端代码
|
||||||
@ -57,6 +84,8 @@ lottery/
|
|||||||
│ │ ├── components/ # 组件
|
│ │ ├── components/ # 组件
|
||||||
│ │ ├── router/ # 路由配置
|
│ │ ├── router/ # 路由配置
|
||||||
│ │ ├── views/ # 页面
|
│ │ ├── views/ # 页面
|
||||||
|
│ │ │ ├── AdvancedAnalysis.vue # 高级分析页面 ⭐
|
||||||
|
│ │ │ └── Prediction.vue # 预测页面 ⭐
|
||||||
│ │ ├── App.vue # 根组件
|
│ │ ├── App.vue # 根组件
|
||||||
│ │ └── main.js # 入口文件
|
│ │ └── main.js # 入口文件
|
||||||
│ ├── package.json # 依赖配置
|
│ ├── package.json # 依赖配置
|
||||||
@ -67,7 +96,7 @@ lottery/
|
|||||||
## 开发环境要求
|
## 开发环境要求
|
||||||
- Python 3.8+
|
- Python 3.8+
|
||||||
- Node.js 16+
|
- Node.js 16+
|
||||||
- PostgreSQL 12+
|
- MySQL 8.0+ 或 PostgreSQL 12+
|
||||||
- npm 或 yarn
|
- npm 或 yarn
|
||||||
|
|
||||||
## 安装和运行
|
## 安装和运行
|
||||||
@ -87,7 +116,7 @@ pip install -r requirements.txt
|
|||||||
```
|
```
|
||||||
|
|
||||||
3. 配置数据库
|
3. 配置数据库
|
||||||
- 创建 PostgreSQL 数据库
|
- 创建 MySQL/PostgreSQL 数据库
|
||||||
- 修改 `backend/app/core/database.py` 中的数据库连接配置
|
- 修改 `backend/app/core/database.py` 中的数据库连接配置
|
||||||
|
|
||||||
4. 启动服务
|
4. 启动服务
|
||||||
@ -142,9 +171,30 @@ python schedule_update.py
|
|||||||
```
|
```
|
||||||
系统会在每天凌晨2点自动检查并更新数据。
|
系统会在每天凌晨2点自动检查并更新数据。
|
||||||
|
|
||||||
#### 其他说明
|
### 高级分析功能 ⭐
|
||||||
- 首页、API、前端等所有"最新开奖"展示均以 `open_time` 最大值为准,保证数据准确。
|
1. 进入"高级分析"页面
|
||||||
- 数据库不会因期号异常导致遗漏或重复,所有唯一性、顺序均以开奖日期为核心。
|
2. 选择彩票类型(双色球/大乐透)
|
||||||
|
3. 选择分析类型:
|
||||||
|
- **遗漏值分析**: 查看各号码的遗漏期数
|
||||||
|
- **和值分析**: 分析红球和值的分布规律
|
||||||
|
- **AC值分析**: 邻号差值分析
|
||||||
|
- **质合比分析**: 质数与合数比例
|
||||||
|
- **012路分析**: 除3余数分布
|
||||||
|
- **跨度分析**: 号码跨度统计
|
||||||
|
- **综合分析**: 多维度整合分析
|
||||||
|
4. 设置分析期数(10-500期)
|
||||||
|
5. 点击"分析"按钮查看结果
|
||||||
|
|
||||||
|
### 智能预测功能 ⭐
|
||||||
|
1. 进入"智能预测"页面
|
||||||
|
2. 选择彩票类型
|
||||||
|
3. 设置训练期数(建议100-500期)
|
||||||
|
4. 点击"训练模型"按钮
|
||||||
|
5. 训练完成后,可选择以下预测方法:
|
||||||
|
- **机器学习预测**: 基于AI算法的预测
|
||||||
|
- **模式预测**: 基于统计模式的预测
|
||||||
|
- **集成预测**: 多方法综合预测
|
||||||
|
6. 查看预测结果和置信度
|
||||||
|
|
||||||
### 数据查询
|
### 数据查询
|
||||||
1. 在查询表单中输入查询条件
|
1. 在查询表单中输入查询条件
|
||||||
@ -167,6 +217,23 @@ python schedule_update.py
|
|||||||
- Swagger UI: http://localhost:8000/docs
|
- Swagger UI: http://localhost:8000/docs
|
||||||
- ReDoc: http://localhost:8000/redoc
|
- ReDoc: http://localhost:8000/redoc
|
||||||
|
|
||||||
|
### 新增API端点 ⭐
|
||||||
|
|
||||||
|
#### 高级分析API
|
||||||
|
- `GET /api/v1/advanced-analysis/missing-value/{lottery_type}` - 遗漏值分析
|
||||||
|
- `GET /api/v1/advanced-analysis/sum-value/{lottery_type}` - 和值分析
|
||||||
|
- `GET /api/v1/advanced-analysis/ac-value/{lottery_type}` - AC值分析
|
||||||
|
- `GET /api/v1/advanced-analysis/prime-composite/{lottery_type}` - 质合比分析
|
||||||
|
- `GET /api/v1/advanced-analysis/road-012/{lottery_type}` - 012路分析
|
||||||
|
- `GET /api/v1/advanced-analysis/span/{lottery_type}` - 跨度分析
|
||||||
|
- `GET /api/v1/advanced-analysis/comprehensive/{lottery_type}` - 综合分析
|
||||||
|
|
||||||
|
#### 预测API
|
||||||
|
- `POST /api/v1/prediction/train/{lottery_type}` - 训练预测模型
|
||||||
|
- `GET /api/v1/prediction/predict/{lottery_type}` - 机器学习预测
|
||||||
|
- `GET /api/v1/prediction/pattern/{lottery_type}` - 模式预测
|
||||||
|
- `GET /api/v1/prediction/ensemble/{lottery_type}` - 集成预测
|
||||||
|
|
||||||
## 常见问题
|
## 常见问题
|
||||||
1. 数据库连接失败
|
1. 数据库连接失败
|
||||||
- 检查数据库服务是否启动
|
- 检查数据库服务是否启动
|
||||||
@ -180,12 +247,21 @@ python schedule_update.py
|
|||||||
- 检查 JSON 文件格式是否正确
|
- 检查 JSON 文件格式是否正确
|
||||||
- 确认数据库表结构是否完整
|
- 确认数据库表结构是否完整
|
||||||
|
|
||||||
|
4. 机器学习预测失败
|
||||||
|
- 确保历史数据充足(建议至少100期)
|
||||||
|
- 检查训练参数设置是否合理
|
||||||
|
|
||||||
## 开发计划
|
## 开发计划
|
||||||
|
- [x] 添加高级数据分析功能
|
||||||
|
- [x] 实现机器学习预测系统
|
||||||
|
- [x] 优化数据可视化展示
|
||||||
- [ ] 添加用户认证功能
|
- [ ] 添加用户认证功能
|
||||||
- [ ] 实现数据备份和恢复
|
- [ ] 实现数据备份和恢复
|
||||||
- [ ] 优化数据导入性能
|
- [ ] 优化数据导入性能
|
||||||
- [ ] 添加更多统计分析功能
|
- [ ] 添加更多统计分析功能
|
||||||
- [ ] 实现自定义选号策略
|
- [ ] 实现自定义选号策略
|
||||||
|
- [ ] 添加移动端适配
|
||||||
|
- [ ] 实现数据导出功能增强
|
||||||
|
|
||||||
## 贡献指南
|
## 贡献指南
|
||||||
1. Fork 项目
|
1. Fork 项目
|
||||||
@ -197,84 +273,20 @@ python schedule_update.py
|
|||||||
## 许可证
|
## 许可证
|
||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
## 数据更新功能
|
## 更新日志
|
||||||
|
|
||||||
系统支持自动从聚合数据API获取最新的开奖数据并更新到本地数据库。更新功能包括:
|
### v2.0.0 (2024-01-XX)
|
||||||
|
- ✨ 新增高级数据分析功能
|
||||||
|
- ✨ 新增机器学习预测系统
|
||||||
|
- ✨ 新增遗漏值、和值、AC值等分析
|
||||||
|
- ✨ 新增质合比、012路、跨度分析
|
||||||
|
- ✨ 优化数据可视化展示
|
||||||
|
- 🔧 更新依赖包版本
|
||||||
|
- 📝 完善API文档
|
||||||
|
|
||||||
1. 手动更新
|
### v1.0.0 (2024-01-XX)
|
||||||
```bash
|
- 🎉 初始版本发布
|
||||||
cd backend
|
- ✨ 基础数据管理功能
|
||||||
python update_lottery.py
|
- ✨ 基础统计分析
|
||||||
```
|
- ✨ 智能选号功能
|
||||||
|
- ✨ 前后端分离架构
|
||||||
2. 自动更新
|
|
||||||
```bash
|
|
||||||
cd backend
|
|
||||||
python schedule_update.py
|
|
||||||
```
|
|
||||||
系统会在每天凌晨2点自动检查并更新数据。
|
|
||||||
|
|
||||||
### 数据更新说明
|
|
||||||
|
|
||||||
- 系统会自动检查本地数据库中最新的开奖日期
|
|
||||||
- 只获取并更新比本地数据更新的开奖记录
|
|
||||||
- 更新过程会记录日志到 `lottery_update.log` 文件
|
|
||||||
- 支持双色球和大乐透两种彩票的数据更新
|
|
||||||
|
|
||||||
## 分析功能说明
|
|
||||||
|
|
||||||
### 基础分析策略
|
|
||||||
|
|
||||||
#### 1. 冷热号码分析
|
|
||||||
- 热号:统计近50期出现频率最高的号码
|
|
||||||
- 冷号:统计超过平均遗漏期数的号码
|
|
||||||
- API: GET `/api/analysis/hot-cold/{lottery_type}?periods=50`
|
|
||||||
|
|
||||||
#### 2. 号码分布分析
|
|
||||||
- 分区统计:将号码划分为多个区间,分析各区出号比例
|
|
||||||
- 奇偶比:分析奇偶数的分布规律
|
|
||||||
- 大小比:统计大数和小数的分布规律
|
|
||||||
- API: GET `/api/analysis/distribution/{lottery_type}?periods=100`
|
|
||||||
|
|
||||||
#### 3. 连号与重复号分析
|
|
||||||
- 连号追踪:统计连号出现的频率和位置
|
|
||||||
- 重复号观察:分析相邻期号码重复情况
|
|
||||||
- API: GET `/api/analysis/consecutive/{lottery_type}?periods=100`
|
|
||||||
|
|
||||||
### 数学理论应用
|
|
||||||
|
|
||||||
#### 1. 数学统计特征
|
|
||||||
- 计算红球总和的历史平均值和标准差
|
|
||||||
- 分析号码组合的数学特征
|
|
||||||
- API: GET `/api/analysis/math-stats/{lottery_type}?periods=100`
|
|
||||||
|
|
||||||
#### 2. 遗漏值分析
|
|
||||||
- 计算每个号码的当前遗漏值
|
|
||||||
- 分析历史最大遗漏
|
|
||||||
- API: GET `/api/analysis/missing/{lottery_type}`
|
|
||||||
|
|
||||||
### 智能选号策略
|
|
||||||
|
|
||||||
系统提供多种智能选号策略:
|
|
||||||
- 均衡策略:综合考虑号码分布特征
|
|
||||||
- 热号策略:偏好选择近期高频号码
|
|
||||||
- 冷号策略:偏好选择遗漏值较大的号码
|
|
||||||
- 遗漏值策略:基于遗漏值分析选号
|
|
||||||
- API: GET `/api/analysis/smart-numbers/{lottery_type}?strategy=balanced`
|
|
||||||
|
|
||||||
### 使用示例
|
|
||||||
|
|
||||||
1. 获取双色球热号冷号分析:
|
|
||||||
```bash
|
|
||||||
curl http://localhost:8000/api/analysis/hot-cold/ssq?periods=50
|
|
||||||
```
|
|
||||||
|
|
||||||
2. 获取大乐透号码分布分析:
|
|
||||||
```bash
|
|
||||||
curl http://localhost:8000/api/analysis/distribution/dlt?periods=100
|
|
||||||
```
|
|
||||||
|
|
||||||
3. 使用均衡策略生成双色球号码:
|
|
||||||
```bash
|
|
||||||
curl http://localhost:8000/api/analysis/smart-numbers/ssq?strategy=balanced
|
|
||||||
```
|
|
||||||
105
backend/app/api/endpoints/advanced_analysis.py
Normal file
105
backend/app/api/endpoints/advanced_analysis.py
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from typing import Dict
|
||||||
|
from ...core.database import get_db
|
||||||
|
from ...services.advanced_analysis import AdvancedAnalysisService
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/missing-value/{lottery_type}")
|
||||||
|
def get_missing_value_analysis(
|
||||||
|
lottery_type: str,
|
||||||
|
periods: int = 100,
|
||||||
|
db: Session = Depends(get_db)
|
||||||
|
):
|
||||||
|
"""获取遗漏值分析"""
|
||||||
|
if lottery_type not in ['ssq', 'dlt']:
|
||||||
|
raise HTTPException(status_code=400, detail="Invalid lottery type")
|
||||||
|
|
||||||
|
service = AdvancedAnalysisService(db)
|
||||||
|
return service.get_missing_value_analysis(lottery_type, periods)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/sum-value/{lottery_type}")
|
||||||
|
def get_sum_value_analysis(
|
||||||
|
lottery_type: str,
|
||||||
|
periods: int = 100,
|
||||||
|
db: Session = Depends(get_db)
|
||||||
|
):
|
||||||
|
"""获取和值分析"""
|
||||||
|
if lottery_type not in ['ssq', 'dlt']:
|
||||||
|
raise HTTPException(status_code=400, detail="Invalid lottery type")
|
||||||
|
|
||||||
|
service = AdvancedAnalysisService(db)
|
||||||
|
return service.get_sum_value_analysis(lottery_type, periods)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/ac-value/{lottery_type}")
|
||||||
|
def get_ac_value_analysis(
|
||||||
|
lottery_type: str,
|
||||||
|
periods: int = 100,
|
||||||
|
db: Session = Depends(get_db)
|
||||||
|
):
|
||||||
|
"""获取AC值分析"""
|
||||||
|
if lottery_type not in ['ssq', 'dlt']:
|
||||||
|
raise HTTPException(status_code=400, detail="Invalid lottery type")
|
||||||
|
|
||||||
|
service = AdvancedAnalysisService(db)
|
||||||
|
return service.get_ac_value_analysis(lottery_type, periods)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/prime-composite/{lottery_type}")
|
||||||
|
def get_prime_composite_analysis(
|
||||||
|
lottery_type: str,
|
||||||
|
periods: int = 100,
|
||||||
|
db: Session = Depends(get_db)
|
||||||
|
):
|
||||||
|
"""获取质合比分析"""
|
||||||
|
if lottery_type not in ['ssq', 'dlt']:
|
||||||
|
raise HTTPException(status_code=400, detail="Invalid lottery type")
|
||||||
|
|
||||||
|
service = AdvancedAnalysisService(db)
|
||||||
|
return service.get_prime_composite_analysis(lottery_type, periods)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/road-012/{lottery_type}")
|
||||||
|
def get_road_012_analysis(
|
||||||
|
lottery_type: str,
|
||||||
|
periods: int = 100,
|
||||||
|
db: Session = Depends(get_db)
|
||||||
|
):
|
||||||
|
"""获取012路分析"""
|
||||||
|
if lottery_type not in ['ssq', 'dlt']:
|
||||||
|
raise HTTPException(status_code=400, detail="Invalid lottery type")
|
||||||
|
|
||||||
|
service = AdvancedAnalysisService(db)
|
||||||
|
return service.get_road_012_analysis(lottery_type, periods)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/span/{lottery_type}")
|
||||||
|
def get_span_analysis(
|
||||||
|
lottery_type: str,
|
||||||
|
periods: int = 100,
|
||||||
|
db: Session = Depends(get_db)
|
||||||
|
):
|
||||||
|
"""获取跨度分析"""
|
||||||
|
if lottery_type not in ['ssq', 'dlt']:
|
||||||
|
raise HTTPException(status_code=400, detail="Invalid lottery type")
|
||||||
|
|
||||||
|
service = AdvancedAnalysisService(db)
|
||||||
|
return service.get_span_analysis(lottery_type, periods)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/comprehensive/{lottery_type}")
|
||||||
|
def get_comprehensive_analysis(
|
||||||
|
lottery_type: str,
|
||||||
|
periods: int = 100,
|
||||||
|
db: Session = Depends(get_db)
|
||||||
|
):
|
||||||
|
"""获取综合分析"""
|
||||||
|
if lottery_type not in ['ssq', 'dlt']:
|
||||||
|
raise HTTPException(status_code=400, detail="Invalid lottery type")
|
||||||
|
|
||||||
|
service = AdvancedAnalysisService(db)
|
||||||
|
return service.get_comprehensive_analysis(lottery_type, periods)
|
||||||
63
backend/app/api/endpoints/prediction.py
Normal file
63
backend/app/api/endpoints/prediction.py
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from typing import Dict
|
||||||
|
from ...core.database import get_db
|
||||||
|
from ...services.prediction_service import PredictionService
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/train/{lottery_type}")
|
||||||
|
def train_prediction_model(
|
||||||
|
lottery_type: str,
|
||||||
|
periods: int = 100,
|
||||||
|
db: Session = Depends(get_db)
|
||||||
|
):
|
||||||
|
"""训练预测模型"""
|
||||||
|
if lottery_type not in ['ssq', 'dlt']:
|
||||||
|
raise HTTPException(status_code=400, detail="Invalid lottery type")
|
||||||
|
|
||||||
|
service = PredictionService(db)
|
||||||
|
return service.train_model(lottery_type, periods)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/predict/{lottery_type}")
|
||||||
|
def predict_next_numbers(
|
||||||
|
lottery_type: str,
|
||||||
|
periods: int = 10,
|
||||||
|
db: Session = Depends(get_db)
|
||||||
|
):
|
||||||
|
"""预测下一期号码"""
|
||||||
|
if lottery_type not in ['ssq', 'dlt']:
|
||||||
|
raise HTTPException(status_code=400, detail="Invalid lottery type")
|
||||||
|
|
||||||
|
service = PredictionService(db)
|
||||||
|
return service.predict_next_numbers(lottery_type, periods)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/pattern/{lottery_type}")
|
||||||
|
def get_pattern_prediction(
|
||||||
|
lottery_type: str,
|
||||||
|
periods: int = 100,
|
||||||
|
db: Session = Depends(get_db)
|
||||||
|
):
|
||||||
|
"""基于模式的预测"""
|
||||||
|
if lottery_type not in ['ssq', 'dlt']:
|
||||||
|
raise HTTPException(status_code=400, detail="Invalid lottery type")
|
||||||
|
|
||||||
|
service = PredictionService(db)
|
||||||
|
return service.get_pattern_based_prediction(lottery_type, periods)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/ensemble/{lottery_type}")
|
||||||
|
def get_ensemble_prediction(
|
||||||
|
lottery_type: str,
|
||||||
|
periods: int = 100,
|
||||||
|
db: Session = Depends(get_db)
|
||||||
|
):
|
||||||
|
"""集成预测"""
|
||||||
|
if lottery_type not in ['ssq', 'dlt']:
|
||||||
|
raise HTTPException(status_code=400, detail="Invalid lottery type")
|
||||||
|
|
||||||
|
service = PredictionService(db)
|
||||||
|
return service.get_ensemble_prediction(lottery_type, periods)
|
||||||
@ -3,6 +3,8 @@ from fastapi.middleware.cors import CORSMiddleware
|
|||||||
from app.core.config import settings
|
from app.core.config import settings
|
||||||
from app.api.v1.lottery import router as lottery_router
|
from app.api.v1.lottery import router as lottery_router
|
||||||
from app.api.endpoints.analysis import router as analysis_router
|
from app.api.endpoints.analysis import router as analysis_router
|
||||||
|
from app.api.endpoints.advanced_analysis import router as advanced_analysis_router
|
||||||
|
from app.api.endpoints.prediction import router as prediction_router
|
||||||
from app.core.database import Base, engine
|
from app.core.database import Base, engine
|
||||||
|
|
||||||
# 创建数据库表
|
# 创建数据库表
|
||||||
@ -27,6 +29,10 @@ app.include_router(
|
|||||||
lottery_router, prefix=f"{settings.API_V1_STR}/lottery", tags=["lottery"])
|
lottery_router, prefix=f"{settings.API_V1_STR}/lottery", tags=["lottery"])
|
||||||
app.include_router(
|
app.include_router(
|
||||||
analysis_router, prefix=f"{settings.API_V1_STR}/analysis", tags=["analysis"])
|
analysis_router, prefix=f"{settings.API_V1_STR}/analysis", tags=["analysis"])
|
||||||
|
app.include_router(
|
||||||
|
advanced_analysis_router, prefix=f"{settings.API_V1_STR}/advanced-analysis", tags=["advanced-analysis"])
|
||||||
|
app.include_router(
|
||||||
|
prediction_router, prefix=f"{settings.API_V1_STR}/prediction", tags=["prediction"])
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
import uvicorn
|
import uvicorn
|
||||||
|
|||||||
201
backend/app/services/advanced_analysis.py
Normal file
201
backend/app/services/advanced_analysis.py
Normal file
@ -0,0 +1,201 @@
|
|||||||
|
from typing import Dict
|
||||||
|
import numpy as np
|
||||||
|
from collections import defaultdict
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from ..models.lottery import SSQLottery, DLTLottery
|
||||||
|
|
||||||
|
|
||||||
|
class AdvancedAnalysisService:
|
||||||
|
def __init__(self, db: Session):
|
||||||
|
self.db = db
|
||||||
|
|
||||||
|
def get_missing_value_analysis(self, lottery_type: str, periods: int = 100) -> Dict:
|
||||||
|
model = SSQLottery if lottery_type == 'ssq' else DLTLottery
|
||||||
|
recent_draws = self.db.query(model).order_by(
|
||||||
|
model.open_time.desc()).limit(periods).all()
|
||||||
|
if lottery_type == 'ssq':
|
||||||
|
red_range = 33
|
||||||
|
blue_range = 16
|
||||||
|
else:
|
||||||
|
red_range = 35
|
||||||
|
blue_range = 12
|
||||||
|
red_missing = {i: 0 for i in range(1, red_range + 1)}
|
||||||
|
blue_missing = {i: 0 for i in range(1, blue_range + 1)}
|
||||||
|
for draw in reversed(recent_draws):
|
||||||
|
if lottery_type == 'ssq':
|
||||||
|
red_numbers = [draw.red_ball_1, draw.red_ball_2, draw.red_ball_3,
|
||||||
|
draw.red_ball_4, draw.red_ball_5, draw.red_ball_6]
|
||||||
|
blue_numbers = [draw.blue_ball]
|
||||||
|
else:
|
||||||
|
red_numbers = [draw.front_ball_1, draw.front_ball_2, draw.front_ball_3,
|
||||||
|
draw.front_ball_4, draw.front_ball_5]
|
||||||
|
blue_numbers = [draw.back_ball_1, draw.back_ball_2]
|
||||||
|
for num in range(1, red_range + 1):
|
||||||
|
if num not in red_numbers:
|
||||||
|
red_missing[num] += 1
|
||||||
|
for num in range(1, blue_range + 1):
|
||||||
|
if num not in blue_numbers:
|
||||||
|
blue_missing[num] += 1
|
||||||
|
return {
|
||||||
|
'red_missing': red_missing,
|
||||||
|
'blue_missing': blue_missing,
|
||||||
|
'max_red_missing': max(red_missing.values()),
|
||||||
|
'max_blue_missing': max(blue_missing.values()),
|
||||||
|
'avg_red_missing': sum(red_missing.values()) / len(red_missing),
|
||||||
|
'avg_blue_missing': sum(blue_missing.values()) / len(blue_missing)
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_sum_value_analysis(self, lottery_type: str, periods: int = 100) -> Dict:
|
||||||
|
model = SSQLottery if lottery_type == 'ssq' else DLTLottery
|
||||||
|
recent_draws = self.db.query(model).order_by(
|
||||||
|
model.open_time.desc()).limit(periods).all()
|
||||||
|
sums = []
|
||||||
|
for draw in recent_draws:
|
||||||
|
if lottery_type == 'ssq':
|
||||||
|
red_numbers = [draw.red_ball_1, draw.red_ball_2, draw.red_ball_3,
|
||||||
|
draw.red_ball_4, draw.red_ball_5, draw.red_ball_6]
|
||||||
|
else:
|
||||||
|
red_numbers = [draw.front_ball_1, draw.front_ball_2, draw.front_ball_3,
|
||||||
|
draw.front_ball_4, draw.front_ball_5]
|
||||||
|
sums.append(sum(red_numbers))
|
||||||
|
sum_distribution = defaultdict(int)
|
||||||
|
for s in sums:
|
||||||
|
sum_distribution[s] += 1
|
||||||
|
return {
|
||||||
|
'sums': sums,
|
||||||
|
'sum_distribution': dict(sum_distribution),
|
||||||
|
'min_sum': min(sums),
|
||||||
|
'max_sum': max(sums),
|
||||||
|
'avg_sum': float(np.mean(sums)),
|
||||||
|
'median_sum': float(np.median(sums)),
|
||||||
|
'std_sum': float(np.std(sums)),
|
||||||
|
'most_common_sums': sorted(sum_distribution.items(), key=lambda x: x[1], reverse=True)[:10]
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_ac_value_analysis(self, lottery_type: str, periods: int = 100) -> Dict:
|
||||||
|
model = SSQLottery if lottery_type == 'ssq' else DLTLottery
|
||||||
|
recent_draws = self.db.query(model).order_by(
|
||||||
|
model.open_time.desc()).limit(periods).all()
|
||||||
|
ac_values = []
|
||||||
|
for draw in recent_draws:
|
||||||
|
if lottery_type == 'ssq':
|
||||||
|
red_numbers = sorted([draw.red_ball_1, draw.red_ball_2, draw.red_ball_3,
|
||||||
|
draw.red_ball_4, draw.red_ball_5, draw.red_ball_6])
|
||||||
|
else:
|
||||||
|
red_numbers = sorted([draw.front_ball_1, draw.front_ball_2, draw.front_ball_3,
|
||||||
|
draw.front_ball_4, draw.front_ball_5])
|
||||||
|
ac = sum(abs(red_numbers[i+1] - red_numbers[i])
|
||||||
|
for i in range(len(red_numbers)-1))
|
||||||
|
ac_values.append(ac)
|
||||||
|
ac_distribution = defaultdict(int)
|
||||||
|
for ac in ac_values:
|
||||||
|
ac_distribution[ac] += 1
|
||||||
|
return {
|
||||||
|
'ac_values': ac_values,
|
||||||
|
'ac_distribution': dict(ac_distribution),
|
||||||
|
'min_ac': min(ac_values),
|
||||||
|
'max_ac': max(ac_values),
|
||||||
|
'avg_ac': float(np.mean(ac_values)),
|
||||||
|
'median_ac': float(np.median(ac_values)),
|
||||||
|
'std_ac': float(np.std(ac_values)),
|
||||||
|
'most_common_ac': sorted(ac_distribution.items(), key=lambda x: x[1], reverse=True)[:10]
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_prime_composite_analysis(self, lottery_type: str, periods: int = 100) -> Dict:
|
||||||
|
def is_prime(n):
|
||||||
|
if n < 2:
|
||||||
|
return False
|
||||||
|
for i in range(2, int(n**0.5) + 1):
|
||||||
|
if n % i == 0:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
model = SSQLottery if lottery_type == 'ssq' else DLTLottery
|
||||||
|
recent_draws = self.db.query(model).order_by(
|
||||||
|
model.open_time.desc()).limit(periods).all()
|
||||||
|
prime_ratios = []
|
||||||
|
for draw in recent_draws:
|
||||||
|
if lottery_type == 'ssq':
|
||||||
|
red_numbers = [draw.red_ball_1, draw.red_ball_2, draw.red_ball_3,
|
||||||
|
draw.red_ball_4, draw.red_ball_5, draw.red_ball_6]
|
||||||
|
else:
|
||||||
|
red_numbers = [draw.front_ball_1, draw.front_ball_2, draw.front_ball_3,
|
||||||
|
draw.front_ball_4, draw.front_ball_5]
|
||||||
|
prime_count = sum(1 for num in red_numbers if is_prime(num))
|
||||||
|
composite_count = len(red_numbers) - prime_count
|
||||||
|
prime_ratios.append(f"{prime_count}:{composite_count}")
|
||||||
|
ratio_distribution = defaultdict(int)
|
||||||
|
for ratio in prime_ratios:
|
||||||
|
ratio_distribution[ratio] += 1
|
||||||
|
return {
|
||||||
|
'prime_ratios': prime_ratios,
|
||||||
|
'ratio_distribution': dict(ratio_distribution),
|
||||||
|
'most_common_ratios': sorted(ratio_distribution.items(), key=lambda x: x[1], reverse=True)[:10]
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_road_012_analysis(self, lottery_type: str, periods: int = 100) -> Dict:
|
||||||
|
model = SSQLottery if lottery_type == 'ssq' else DLTLottery
|
||||||
|
recent_draws = self.db.query(model).order_by(
|
||||||
|
model.open_time.desc()).limit(periods).all()
|
||||||
|
road_012_counts = []
|
||||||
|
for draw in recent_draws:
|
||||||
|
if lottery_type == 'ssq':
|
||||||
|
red_numbers = [draw.red_ball_1, draw.red_ball_2, draw.red_ball_3,
|
||||||
|
draw.red_ball_4, draw.red_ball_5, draw.red_ball_6]
|
||||||
|
else:
|
||||||
|
red_numbers = [draw.front_ball_1, draw.front_ball_2, draw.front_ball_3,
|
||||||
|
draw.front_ball_4, draw.front_ball_5]
|
||||||
|
road_0 = sum(1 for num in red_numbers if num % 3 == 0)
|
||||||
|
road_1 = sum(1 for num in red_numbers if num % 3 == 1)
|
||||||
|
road_2 = sum(1 for num in red_numbers if num % 3 == 2)
|
||||||
|
road_012_counts.append({
|
||||||
|
'road_0': road_0,
|
||||||
|
'road_1': road_1,
|
||||||
|
'road_2': road_2,
|
||||||
|
'pattern': f"{road_0}:{road_1}:{road_2}"
|
||||||
|
})
|
||||||
|
pattern_distribution = defaultdict(int)
|
||||||
|
for count in road_012_counts:
|
||||||
|
pattern_distribution[count['pattern']] += 1
|
||||||
|
return {
|
||||||
|
'road_012_counts': road_012_counts,
|
||||||
|
'pattern_distribution': dict(pattern_distribution),
|
||||||
|
'most_common_patterns': sorted(pattern_distribution.items(), key=lambda x: x[1], reverse=True)[:10]
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_span_analysis(self, lottery_type: str, periods: int = 100) -> Dict:
|
||||||
|
model = SSQLottery if lottery_type == 'ssq' else DLTLottery
|
||||||
|
recent_draws = self.db.query(model).order_by(
|
||||||
|
model.open_time.desc()).limit(periods).all()
|
||||||
|
spans = []
|
||||||
|
for draw in recent_draws:
|
||||||
|
if lottery_type == 'ssq':
|
||||||
|
red_numbers = [draw.red_ball_1, draw.red_ball_2, draw.red_ball_3,
|
||||||
|
draw.red_ball_4, draw.red_ball_5, draw.red_ball_6]
|
||||||
|
else:
|
||||||
|
red_numbers = [draw.front_ball_1, draw.front_ball_2, draw.front_ball_3,
|
||||||
|
draw.front_ball_4, draw.front_ball_5]
|
||||||
|
span = max(red_numbers) - min(red_numbers)
|
||||||
|
spans.append(span)
|
||||||
|
span_distribution = defaultdict(int)
|
||||||
|
for span in spans:
|
||||||
|
span_distribution[span] += 1
|
||||||
|
return {
|
||||||
|
'spans': spans,
|
||||||
|
'span_distribution': dict(span_distribution),
|
||||||
|
'min_span': min(spans),
|
||||||
|
'max_span': max(spans),
|
||||||
|
'avg_span': float(np.mean(spans)),
|
||||||
|
'median_span': float(np.median(spans)),
|
||||||
|
'std_span': float(np.std(spans)),
|
||||||
|
'most_common_spans': sorted(span_distribution.items(), key=lambda x: x[1], reverse=True)[:10]
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_comprehensive_analysis(self, lottery_type: str, periods: int = 100) -> Dict:
|
||||||
|
return {
|
||||||
|
'missing_value': self.get_missing_value_analysis(lottery_type, periods),
|
||||||
|
'sum_value': self.get_sum_value_analysis(lottery_type, periods),
|
||||||
|
'ac_value': self.get_ac_value_analysis(lottery_type, periods),
|
||||||
|
'prime_composite': self.get_prime_composite_analysis(lottery_type, periods),
|
||||||
|
'road_012': self.get_road_012_analysis(lottery_type, periods),
|
||||||
|
'span': self.get_span_analysis(lottery_type, periods)
|
||||||
|
}
|
||||||
535
backend/app/services/prediction_service.py
Normal file
535
backend/app/services/prediction_service.py
Normal file
@ -0,0 +1,535 @@
|
|||||||
|
from typing import List, Dict, Tuple, Optional
|
||||||
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
|
from sklearn.ensemble import RandomForestRegressor
|
||||||
|
from sklearn.preprocessing import StandardScaler
|
||||||
|
from sklearn.model_selection import train_test_split
|
||||||
|
from collections import defaultdict
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from ..models.lottery import SSQLottery, DLTLottery
|
||||||
|
|
||||||
|
|
||||||
|
class PredictionService:
|
||||||
|
# 类级别的字典来存储所有模型
|
||||||
|
_models = {}
|
||||||
|
_scalers = {}
|
||||||
|
|
||||||
|
def __init__(self, db: Session):
|
||||||
|
self.db = db
|
||||||
|
|
||||||
|
def prepare_features(self, lottery_type: str, periods: int = 100) -> Tuple[np.ndarray, np.ndarray]:
|
||||||
|
"""准备机器学习特征
|
||||||
|
|
||||||
|
Args:
|
||||||
|
lottery_type: 彩票类型 ('ssq' 或 'dlt')
|
||||||
|
periods: 使用期数
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple: (特征矩阵, 标签矩阵)
|
||||||
|
"""
|
||||||
|
model = SSQLottery if lottery_type == 'ssq' else DLTLottery
|
||||||
|
recent_draws = self.db.query(model).order_by(
|
||||||
|
model.open_time.desc()).limit(periods).all()
|
||||||
|
|
||||||
|
features = []
|
||||||
|
labels = []
|
||||||
|
|
||||||
|
for i in range(len(recent_draws) - 10): # 使用前10期预测下一期
|
||||||
|
# 特征:前10期的号码
|
||||||
|
feature_row = []
|
||||||
|
for j in range(10):
|
||||||
|
draw = recent_draws[i + j]
|
||||||
|
if lottery_type == 'ssq':
|
||||||
|
numbers = [draw.red_ball_1, draw.red_ball_2, draw.red_ball_3,
|
||||||
|
draw.red_ball_4, draw.red_ball_5, draw.red_ball_6]
|
||||||
|
feature_row.extend(sorted(numbers))
|
||||||
|
feature_row.append(draw.blue_ball) # 添加蓝球
|
||||||
|
else:
|
||||||
|
numbers = [draw.front_ball_1, draw.front_ball_2, draw.front_ball_3,
|
||||||
|
draw.front_ball_4, draw.front_ball_5]
|
||||||
|
feature_row.extend(sorted(numbers))
|
||||||
|
feature_row.extend(
|
||||||
|
[draw.back_ball_1, draw.back_ball_2]) # 添加后区号码
|
||||||
|
|
||||||
|
# 标签:下一期的号码
|
||||||
|
next_draw = recent_draws[i + 10]
|
||||||
|
if lottery_type == 'ssq':
|
||||||
|
label_numbers = [next_draw.red_ball_1, next_draw.red_ball_2, next_draw.red_ball_3,
|
||||||
|
next_draw.red_ball_4, next_draw.red_ball_5, next_draw.red_ball_6]
|
||||||
|
label_numbers.append(next_draw.blue_ball) # 添加蓝球
|
||||||
|
else:
|
||||||
|
label_numbers = [next_draw.front_ball_1, next_draw.front_ball_2, next_draw.front_ball_3,
|
||||||
|
next_draw.front_ball_4, next_draw.front_ball_5]
|
||||||
|
label_numbers.extend(
|
||||||
|
[next_draw.back_ball_1, next_draw.back_ball_2]) # 添加后区号码
|
||||||
|
|
||||||
|
features.append(feature_row)
|
||||||
|
# 保持红球排序,蓝球位置不变
|
||||||
|
labels.append(sorted(label_numbers[:-1]) + [label_numbers[-1]])
|
||||||
|
|
||||||
|
return np.array(features), np.array(labels)
|
||||||
|
|
||||||
|
def train_model(self, lottery_type: str, periods: int = 100) -> Dict:
|
||||||
|
"""训练预测模型
|
||||||
|
|
||||||
|
Args:
|
||||||
|
lottery_type: 彩票类型 ('ssq' 或 'dlt')
|
||||||
|
periods: 使用期数
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict: 训练结果
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
features, labels = self.prepare_features(lottery_type, periods)
|
||||||
|
|
||||||
|
if len(features) < 20: # 数据不足
|
||||||
|
return {"success": False, "message": "数据不足,无法训练模型"}
|
||||||
|
|
||||||
|
# 标准化特征
|
||||||
|
scaler = StandardScaler()
|
||||||
|
features_scaled = scaler.fit_transform(features)
|
||||||
|
self._scalers[lottery_type] = scaler
|
||||||
|
|
||||||
|
# 为每个号码位置训练一个模型
|
||||||
|
models = {}
|
||||||
|
accuracies = []
|
||||||
|
|
||||||
|
for pos in range(labels.shape[1]):
|
||||||
|
model = RandomForestRegressor(
|
||||||
|
n_estimators=100, random_state=42)
|
||||||
|
X_train, X_test, y_train, y_test = train_test_split(
|
||||||
|
features_scaled, labels[:, pos], test_size=0.2, random_state=42
|
||||||
|
)
|
||||||
|
model.fit(X_train, y_train)
|
||||||
|
accuracy = model.score(X_test, y_test)
|
||||||
|
|
||||||
|
models[f"pos_{pos}"] = model
|
||||||
|
accuracies.append(accuracy)
|
||||||
|
|
||||||
|
self._models[lottery_type] = models
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"message": "模型训练成功",
|
||||||
|
"avg_accuracy": np.mean(accuracies),
|
||||||
|
"accuracies": accuracies,
|
||||||
|
"training_samples": len(features)
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return {"success": False, "message": f"训练失败: {str(e)}"}
|
||||||
|
|
||||||
|
def predict_next_numbers(self, lottery_type: str, periods: int = 10) -> Dict:
|
||||||
|
"""预测下一期号码
|
||||||
|
|
||||||
|
Args:
|
||||||
|
lottery_type: 彩票类型 ('ssq' 或 'dlt')
|
||||||
|
periods: 使用期数
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict: 预测结果
|
||||||
|
"""
|
||||||
|
if lottery_type not in self._models:
|
||||||
|
return {"success": False, "message": "模型未训练,请先训练模型"}
|
||||||
|
|
||||||
|
try:
|
||||||
|
model = SSQLottery if lottery_type == 'ssq' else DLTLottery
|
||||||
|
recent_draws = self.db.query(model).order_by(
|
||||||
|
model.open_time.desc()).limit(10).all() # 只需要最近10期
|
||||||
|
|
||||||
|
if len(recent_draws) < 10:
|
||||||
|
return {"success": False, "message": "历史数据不足"}
|
||||||
|
|
||||||
|
# 准备特征
|
||||||
|
feature_row = []
|
||||||
|
for draw in recent_draws:
|
||||||
|
if lottery_type == 'ssq':
|
||||||
|
numbers = [draw.red_ball_1, draw.red_ball_2, draw.red_ball_3,
|
||||||
|
draw.red_ball_4, draw.red_ball_5, draw.red_ball_6]
|
||||||
|
feature_row.extend(sorted(numbers))
|
||||||
|
feature_row.append(draw.blue_ball) # 添加蓝球
|
||||||
|
else:
|
||||||
|
numbers = [draw.front_ball_1, draw.front_ball_2, draw.front_ball_3,
|
||||||
|
draw.front_ball_4, draw.front_ball_5]
|
||||||
|
feature_row.extend(sorted(numbers))
|
||||||
|
feature_row.extend(
|
||||||
|
[draw.back_ball_1, draw.back_ball_2]) # 添加后区号码
|
||||||
|
|
||||||
|
# 标准化特征
|
||||||
|
scaler = self._scalers.get(lottery_type)
|
||||||
|
if not scaler:
|
||||||
|
return {"success": False, "message": "模型未训练,请先训练模型"}
|
||||||
|
feature_scaled = scaler.transform([feature_row])
|
||||||
|
|
||||||
|
# 预测每个位置的号码
|
||||||
|
predictions = []
|
||||||
|
models = self._models[lottery_type]
|
||||||
|
|
||||||
|
for pos in range(len(models)):
|
||||||
|
model = models[f"pos_{pos}"]
|
||||||
|
pred = model.predict(feature_scaled)[0]
|
||||||
|
predictions.append(round(pred))
|
||||||
|
|
||||||
|
# 确保预测的号码在有效范围内
|
||||||
|
if lottery_type == 'ssq':
|
||||||
|
max_red = 33
|
||||||
|
max_blue = 16
|
||||||
|
red_count = 6
|
||||||
|
else:
|
||||||
|
max_red = 35
|
||||||
|
max_blue = 12
|
||||||
|
red_count = 5
|
||||||
|
|
||||||
|
# 分离红球和蓝球预测
|
||||||
|
if lottery_type == 'ssq':
|
||||||
|
red_predictions = predictions[:red_count]
|
||||||
|
blue_prediction = predictions[-1]
|
||||||
|
else:
|
||||||
|
red_predictions = predictions[:red_count]
|
||||||
|
blue_predictions = predictions[red_count:]
|
||||||
|
|
||||||
|
# 处理红球
|
||||||
|
red_predictions = [max(1, min(max_red, p))
|
||||||
|
for p in red_predictions]
|
||||||
|
red_predictions = sorted(list(set(red_predictions)))
|
||||||
|
|
||||||
|
# 如果红球不够,补充随机号码
|
||||||
|
while len(red_predictions) < red_count:
|
||||||
|
import random
|
||||||
|
new_num = random.randint(1, max_red)
|
||||||
|
if new_num not in red_predictions:
|
||||||
|
red_predictions.append(new_num)
|
||||||
|
red_predictions = sorted(red_predictions[:red_count])
|
||||||
|
|
||||||
|
# 处理蓝球
|
||||||
|
if lottery_type == 'ssq':
|
||||||
|
blue_prediction = max(1, min(max_blue, blue_prediction))
|
||||||
|
else:
|
||||||
|
blue_predictions = [max(1, min(max_blue, p))
|
||||||
|
for p in blue_predictions]
|
||||||
|
blue_predictions = sorted(list(set(blue_predictions)))
|
||||||
|
while len(blue_predictions) < 2:
|
||||||
|
new_num = random.randint(1, max_blue)
|
||||||
|
if new_num not in blue_predictions:
|
||||||
|
blue_predictions.append(new_num)
|
||||||
|
blue_predictions = sorted(blue_predictions)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"predicted_numbers": red_predictions,
|
||||||
|
"predicted_blue": blue_prediction if lottery_type == 'ssq' else None,
|
||||||
|
"predicted_blues": blue_predictions if lottery_type == 'dlt' else None,
|
||||||
|
"confidence": "基于历史数据的机器学习预测"
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return {"success": False, "message": f"预测失败: {str(e)}"}
|
||||||
|
|
||||||
|
def get_pattern_based_prediction(self, lottery_type: str, periods: int = 100) -> Dict:
|
||||||
|
"""基于模式的预测
|
||||||
|
|
||||||
|
Args:
|
||||||
|
lottery_type: 彩票类型 ('ssq' 或 'dlt')
|
||||||
|
periods: 分析期数
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict: 预测结果
|
||||||
|
"""
|
||||||
|
model = SSQLottery if lottery_type == 'ssq' else DLTLottery
|
||||||
|
recent_draws = self.db.query(model).order_by(
|
||||||
|
model.open_time.desc()).limit(periods).all()
|
||||||
|
|
||||||
|
# 分析最近的开奖模式
|
||||||
|
patterns = {
|
||||||
|
'sum_range': [],
|
||||||
|
'odd_even_ratio': [],
|
||||||
|
'zone_distribution': [],
|
||||||
|
'consecutive_count': []
|
||||||
|
}
|
||||||
|
|
||||||
|
for draw in recent_draws:
|
||||||
|
if lottery_type == 'ssq':
|
||||||
|
numbers = [draw.red_ball_1, draw.red_ball_2, draw.red_ball_3,
|
||||||
|
draw.red_ball_4, draw.red_ball_5, draw.red_ball_6]
|
||||||
|
else:
|
||||||
|
numbers = [draw.front_ball_1, draw.front_ball_2, draw.front_ball_3,
|
||||||
|
draw.front_ball_4, draw.front_ball_5]
|
||||||
|
|
||||||
|
# 和值
|
||||||
|
patterns['sum_range'].append(sum(numbers))
|
||||||
|
|
||||||
|
# 奇偶比
|
||||||
|
odd_count = sum(1 for n in numbers if n % 2 == 1)
|
||||||
|
patterns['odd_even_ratio'].append(
|
||||||
|
f"{odd_count}:{len(numbers)-odd_count}")
|
||||||
|
|
||||||
|
# 分区分布
|
||||||
|
zones = [(n-1)//5 + 1 for n in numbers]
|
||||||
|
zone_count = len(set(zones))
|
||||||
|
patterns['zone_distribution'].append(zone_count)
|
||||||
|
|
||||||
|
# 连号数量
|
||||||
|
sorted_nums = sorted(numbers)
|
||||||
|
consecutive = sum(1 for i in range(len(sorted_nums)-1)
|
||||||
|
if sorted_nums[i+1] - sorted_nums[i] == 1)
|
||||||
|
patterns['consecutive_count'].append(consecutive)
|
||||||
|
|
||||||
|
# 计算最常见的模式
|
||||||
|
most_common_patterns = {}
|
||||||
|
for key, values in patterns.items():
|
||||||
|
if key == 'sum_range':
|
||||||
|
# 和值范围
|
||||||
|
avg_sum = np.mean(values)
|
||||||
|
std_sum = np.std(values)
|
||||||
|
most_common_patterns[key] = {
|
||||||
|
'avg': avg_sum,
|
||||||
|
'std': std_sum,
|
||||||
|
'range': [int(avg_sum - std_sum), int(avg_sum + std_sum)]
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
# 其他模式
|
||||||
|
from collections import Counter
|
||||||
|
counter = Counter(values)
|
||||||
|
most_common_patterns[key] = counter.most_common(3)
|
||||||
|
|
||||||
|
# 根据模式生成推荐号码
|
||||||
|
max_num = 33 if lottery_type == 'ssq' else 35
|
||||||
|
target_count = 6 if lottery_type == 'ssq' else 5
|
||||||
|
|
||||||
|
# 获取推荐的模式
|
||||||
|
target_sum_range = most_common_patterns['sum_range']['range']
|
||||||
|
target_odd_ratio = most_common_patterns['odd_even_ratio'][0][0].split(
|
||||||
|
':')[0] if most_common_patterns['odd_even_ratio'] else "3"
|
||||||
|
target_zones = most_common_patterns['zone_distribution'][0][
|
||||||
|
0] if most_common_patterns['zone_distribution'] else 4
|
||||||
|
target_consecutive = most_common_patterns['consecutive_count'][
|
||||||
|
0][0] if most_common_patterns['consecutive_count'] else 1
|
||||||
|
|
||||||
|
# 生成符合模式的号码
|
||||||
|
import random
|
||||||
|
best_numbers = None
|
||||||
|
best_score = -1
|
||||||
|
|
||||||
|
# 尝试100次生成最符合模式的号码
|
||||||
|
for _ in range(100):
|
||||||
|
# 初始化号码集
|
||||||
|
numbers = set()
|
||||||
|
|
||||||
|
# 确保有连号
|
||||||
|
if target_consecutive > 0:
|
||||||
|
start = random.randint(1, max_num - target_consecutive)
|
||||||
|
for i in range(target_consecutive + 1):
|
||||||
|
if len(numbers) < target_count:
|
||||||
|
numbers.add(start + i)
|
||||||
|
|
||||||
|
# 根据奇偶比例添加号码
|
||||||
|
target_odd = int(target_odd_ratio)
|
||||||
|
current_odd = sum(1 for n in numbers if n % 2 == 1)
|
||||||
|
|
||||||
|
while len(numbers) < target_count:
|
||||||
|
n = random.randint(1, max_num)
|
||||||
|
if n not in numbers:
|
||||||
|
if (n % 2 == 1 and current_odd < target_odd) or \
|
||||||
|
(n % 2 == 0 and (len(numbers) - current_odd) < (target_count - target_odd)):
|
||||||
|
numbers.add(n)
|
||||||
|
if n % 2 == 1:
|
||||||
|
current_odd += 1
|
||||||
|
|
||||||
|
numbers = sorted(list(numbers))
|
||||||
|
|
||||||
|
# 计算当前号码组合的得分
|
||||||
|
score = 0
|
||||||
|
|
||||||
|
# 和值得分
|
||||||
|
current_sum = sum(numbers)
|
||||||
|
if target_sum_range[0] <= current_sum <= target_sum_range[1]:
|
||||||
|
score += 1
|
||||||
|
|
||||||
|
# 奇偶比得分
|
||||||
|
current_odd = sum(1 for n in numbers if n % 2 == 1)
|
||||||
|
if current_odd == int(target_odd_ratio):
|
||||||
|
score += 1
|
||||||
|
|
||||||
|
# 分区得分
|
||||||
|
current_zones = len(set((n-1)//5 + 1 for n in numbers))
|
||||||
|
if current_zones == target_zones:
|
||||||
|
score += 1
|
||||||
|
|
||||||
|
# 连号得分
|
||||||
|
current_consecutive = sum(1 for i in range(
|
||||||
|
len(numbers)-1) if numbers[i+1] - numbers[i] == 1)
|
||||||
|
if current_consecutive == target_consecutive:
|
||||||
|
score += 1
|
||||||
|
|
||||||
|
if score > best_score:
|
||||||
|
best_score = score
|
||||||
|
best_numbers = numbers
|
||||||
|
|
||||||
|
# 如果是双色球,还需要生成蓝球
|
||||||
|
predicted_blue = None
|
||||||
|
if lottery_type == 'ssq':
|
||||||
|
# 分析蓝球规律
|
||||||
|
blue_numbers = [draw.blue_ball for draw in recent_draws]
|
||||||
|
blue_counter = Counter(blue_numbers)
|
||||||
|
# 选择最近出现频率适中的蓝球
|
||||||
|
common_blues = [num for num, _ in blue_counter.most_common(
|
||||||
|
)[len(blue_counter)//3:(len(blue_counter)*2)//3]]
|
||||||
|
if common_blues:
|
||||||
|
predicted_blue = random.choice(common_blues)
|
||||||
|
else:
|
||||||
|
predicted_blue = random.randint(1, 16)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"patterns": most_common_patterns,
|
||||||
|
"suggested_criteria": {
|
||||||
|
"sum_range": most_common_patterns['sum_range']['range'],
|
||||||
|
"odd_even_ratio": most_common_patterns['odd_even_ratio'][0][0] if most_common_patterns['odd_even_ratio'] else "3:3",
|
||||||
|
"zone_distribution": most_common_patterns['zone_distribution'][0][0] if most_common_patterns['zone_distribution'] else 4,
|
||||||
|
"consecutive_count": most_common_patterns['consecutive_count'][0][0] if most_common_patterns['consecutive_count'] else 1
|
||||||
|
},
|
||||||
|
"predicted_numbers": best_numbers,
|
||||||
|
"predicted_blue": predicted_blue if lottery_type == 'ssq' else None
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_ensemble_prediction(self, lottery_type: str, periods: int = 100) -> Dict:
|
||||||
|
"""集成预测(结合多种方法)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
lottery_type: 彩票类型 ('ssq' 或 'dlt')
|
||||||
|
periods: 分析期数
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict: 预测结果
|
||||||
|
"""
|
||||||
|
# 机器学习预测
|
||||||
|
ml_result = self.predict_next_numbers(lottery_type, periods)
|
||||||
|
|
||||||
|
# 模式预测
|
||||||
|
pattern_result = self.get_pattern_based_prediction(
|
||||||
|
lottery_type, periods)
|
||||||
|
|
||||||
|
# 频率预测(基于现有服务)
|
||||||
|
from .analysis_service import LotteryAnalysisService
|
||||||
|
analysis_service = LotteryAnalysisService(self.db)
|
||||||
|
freq_result = analysis_service.get_hot_cold_numbers(
|
||||||
|
lottery_type, periods)
|
||||||
|
|
||||||
|
# 综合推荐
|
||||||
|
recommendations = []
|
||||||
|
|
||||||
|
if ml_result.get('success'):
|
||||||
|
recommendations.append({
|
||||||
|
'method': '机器学习',
|
||||||
|
'numbers': ml_result['predicted_numbers'],
|
||||||
|
'blue': ml_result['predicted_blue'] if lottery_type == 'ssq' else None,
|
||||||
|
'blues': ml_result['predicted_blues'] if lottery_type == 'dlt' else None,
|
||||||
|
'confidence': '高'
|
||||||
|
})
|
||||||
|
|
||||||
|
if freq_result:
|
||||||
|
max_red = 33 if lottery_type == 'ssq' else 35
|
||||||
|
max_blue = 16 if lottery_type == 'ssq' else 12
|
||||||
|
red_count = 6 if lottery_type == 'ssq' else 5
|
||||||
|
|
||||||
|
# 获取热号和冷号
|
||||||
|
hot_reds = freq_result['hot_reds']
|
||||||
|
cold_reds = freq_result['cold_reds']
|
||||||
|
|
||||||
|
# 初始化号码集
|
||||||
|
selected_numbers = set()
|
||||||
|
|
||||||
|
# 从热号中选择2-3个号码
|
||||||
|
hot_count = min(3, len(hot_reds))
|
||||||
|
for num in hot_reds[:hot_count]:
|
||||||
|
selected_numbers.add(num)
|
||||||
|
|
||||||
|
# 从冷号中选择1-2个号码
|
||||||
|
cold_count = min(2, len(cold_reds))
|
||||||
|
for num in cold_reds[:cold_count]:
|
||||||
|
if not any(abs(num - x) == 1 for x in selected_numbers): # 避免连号
|
||||||
|
selected_numbers.add(num)
|
||||||
|
|
||||||
|
# 计算还需要多少个号码
|
||||||
|
remaining = red_count - len(selected_numbers)
|
||||||
|
|
||||||
|
# 获取温号(既不是热号也不是冷号的号码)
|
||||||
|
all_numbers = set(range(1, max_red + 1))
|
||||||
|
warm_numbers = list(all_numbers - set(hot_reds) - set(cold_reds))
|
||||||
|
import random
|
||||||
|
random.shuffle(warm_numbers)
|
||||||
|
|
||||||
|
# 从温号中补充号码
|
||||||
|
for num in warm_numbers:
|
||||||
|
if len(selected_numbers) >= red_count:
|
||||||
|
break
|
||||||
|
# 检查是否会形成连号
|
||||||
|
consecutive_count = sum(
|
||||||
|
1 for x in selected_numbers if abs(num - x) == 1)
|
||||||
|
if consecutive_count <= 1: # 最多允许两个连号
|
||||||
|
selected_numbers.add(num)
|
||||||
|
|
||||||
|
# 如果还不够,从剩余号码中随机选择
|
||||||
|
remaining_numbers = list(all_numbers - selected_numbers)
|
||||||
|
while len(selected_numbers) < red_count:
|
||||||
|
num = random.choice(remaining_numbers)
|
||||||
|
consecutive_count = sum(
|
||||||
|
1 for x in selected_numbers if abs(num - x) == 1)
|
||||||
|
if consecutive_count <= 1:
|
||||||
|
selected_numbers.add(num)
|
||||||
|
remaining_numbers.remove(num)
|
||||||
|
|
||||||
|
# 生成蓝球
|
||||||
|
if lottery_type == 'ssq':
|
||||||
|
if 'hot_blues' in freq_result and freq_result['hot_blues']:
|
||||||
|
# 从热门蓝球中随机选择
|
||||||
|
blue_prediction = random.choice(
|
||||||
|
freq_result['hot_blues'][:3])
|
||||||
|
else:
|
||||||
|
blue_prediction = random.randint(1, max_blue)
|
||||||
|
|
||||||
|
recommendations.append({
|
||||||
|
'method': '热冷号分析',
|
||||||
|
'numbers': sorted(list(selected_numbers)),
|
||||||
|
'blue': blue_prediction,
|
||||||
|
'confidence': '中'
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
# 大乐透后区号码选择
|
||||||
|
blue_predictions = []
|
||||||
|
if 'hot_blues' in freq_result and freq_result['hot_blues']:
|
||||||
|
# 从热门后区号码中选择
|
||||||
|
available_blues = freq_result['hot_blues'][:4] # 取前4个热门号码
|
||||||
|
while len(blue_predictions) < 2 and available_blues:
|
||||||
|
num = random.choice(available_blues)
|
||||||
|
blue_predictions.append(num)
|
||||||
|
available_blues.remove(num)
|
||||||
|
|
||||||
|
# 如果还不够2个,随机补充
|
||||||
|
while len(blue_predictions) < 2:
|
||||||
|
num = random.randint(1, max_blue)
|
||||||
|
if num not in blue_predictions:
|
||||||
|
blue_predictions.append(num)
|
||||||
|
|
||||||
|
recommendations.append({
|
||||||
|
'method': '热冷号分析',
|
||||||
|
'numbers': sorted(list(selected_numbers)),
|
||||||
|
'blues': sorted(blue_predictions),
|
||||||
|
'confidence': '中'
|
||||||
|
})
|
||||||
|
|
||||||
|
if pattern_result and pattern_result.get('success'):
|
||||||
|
recommendations.append({
|
||||||
|
'method': '模式分析',
|
||||||
|
'numbers': pattern_result['predicted_numbers'],
|
||||||
|
'blue': pattern_result['predicted_blue'] if lottery_type == 'ssq' else None,
|
||||||
|
'blues': pattern_result['predicted_blues'] if lottery_type == 'dlt' else None,
|
||||||
|
'confidence': '中'
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"recommendations": recommendations,
|
||||||
|
"pattern_analysis": pattern_result.get('suggested_criteria', {}) if pattern_result else {},
|
||||||
|
"frequency_analysis": freq_result or {}
|
||||||
|
}
|
||||||
@ -12,4 +12,8 @@ python-dotenv==1.0.0
|
|||||||
pandas==2.1.3
|
pandas==2.1.3
|
||||||
aiofiles==23.2.1
|
aiofiles==23.2.1
|
||||||
requests==2.31.0
|
requests==2.31.0
|
||||||
schedule==1.2.1
|
schedule==1.2.1
|
||||||
|
numpy>=1.21.0,<2.0.0
|
||||||
|
scikit-learn>=1.0.0,<2.0.0
|
||||||
|
matplotlib>=3.5.0,<4.0.0
|
||||||
|
seaborn>=0.11.0,<1.0.0
|
||||||
@ -1,119 +1,144 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-container class="app-container">
|
<el-container class="layout-container">
|
||||||
<el-header>
|
<el-header>
|
||||||
<div class="header-left">
|
<div class="header-container">
|
||||||
<h1>彩票数据分析系统</h1>
|
<div class="logo">
|
||||||
</div>
|
<h1>彩票数据分析系统</h1>
|
||||||
<div class="header-right">
|
</div>
|
||||||
<el-menu
|
<el-menu
|
||||||
|
:default-active="activeIndex"
|
||||||
|
class="nav-menu"
|
||||||
mode="horizontal"
|
mode="horizontal"
|
||||||
router
|
router
|
||||||
:default-active="$route.path"
|
background-color="#409EFF"
|
||||||
|
text-color="#fff"
|
||||||
|
active-text-color="#fff"
|
||||||
|
@select="handleSelect"
|
||||||
>
|
>
|
||||||
<el-menu-item index="/">首页</el-menu-item>
|
<el-menu-item index="/">首页</el-menu-item>
|
||||||
<el-menu-item index="/ssq">双色球</el-menu-item>
|
<el-menu-item index="/ssq">双色球</el-menu-item>
|
||||||
<el-menu-item index="/dlt">大乐透</el-menu-item>
|
<el-menu-item index="/dlt">大乐透</el-menu-item>
|
||||||
<el-menu-item index="/statistics">统计分析</el-menu-item>
|
<el-menu-item index="/statistics">统计分析</el-menu-item>
|
||||||
|
<el-menu-item index="/advanced-analysis">高级分析</el-menu-item>
|
||||||
|
<el-menu-item index="/prediction">智能预测</el-menu-item>
|
||||||
<el-menu-item index="/number-generator">智能选号</el-menu-item>
|
<el-menu-item index="/number-generator">智能选号</el-menu-item>
|
||||||
</el-menu>
|
</el-menu>
|
||||||
</div>
|
</div>
|
||||||
</el-header>
|
</el-header>
|
||||||
|
|
||||||
<el-main>
|
<el-main>
|
||||||
<router-view></router-view>
|
<router-view></router-view>
|
||||||
</el-main>
|
</el-main>
|
||||||
|
|
||||||
|
<el-footer>
|
||||||
|
<div class="footer-content">
|
||||||
|
<p>彩票数据分析系统 © 2024</p>
|
||||||
|
</div>
|
||||||
|
</el-footer>
|
||||||
</el-container>
|
</el-container>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, onUnmounted } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { useRoute } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { ElContainer, ElHeader, ElMain, ElMenu, ElMenuItem, ElDrawer, ElButton } from 'element-plus'
|
|
||||||
import { Menu } from '@element-plus/icons-vue'
|
|
||||||
|
|
||||||
const drawer = ref(false)
|
const router = useRouter()
|
||||||
const isMobile = ref(false)
|
const activeIndex = ref('/')
|
||||||
const $route = useRoute()
|
|
||||||
|
|
||||||
const checkMobile = () => {
|
const handleSelect = (key) => {
|
||||||
isMobile.value = window.innerWidth <= 768
|
activeIndex.value = key
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
checkMobile()
|
|
||||||
window.addEventListener('resize', checkMobile)
|
|
||||||
})
|
|
||||||
onUnmounted(() => {
|
|
||||||
window.removeEventListener('resize', checkMobile)
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style>
|
||||||
.app-container {
|
.layout-container {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
}
|
|
||||||
.el-header {
|
|
||||||
background-color: #409EFF;
|
|
||||||
color: white;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
flex-direction: column;
|
||||||
justify-content: space-between;
|
}
|
||||||
padding: 0 20px;
|
|
||||||
|
.header-container {
|
||||||
height: 60px;
|
height: 60px;
|
||||||
box-shadow: 0 2px 8px 0 rgba(64,158,255,0.08);
|
|
||||||
}
|
|
||||||
.header-left h1 {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 22px;
|
|
||||||
font-weight: bold;
|
|
||||||
letter-spacing: 2px;
|
|
||||||
}
|
|
||||||
.header-right {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
padding: 0 20px;
|
||||||
}
|
}
|
||||||
.menu-btn {
|
|
||||||
color: white;
|
.logo {
|
||||||
border: none;
|
margin-right: 40px;
|
||||||
background: transparent;
|
|
||||||
font-size: 22px;
|
|
||||||
}
|
}
|
||||||
.el-menu {
|
|
||||||
background: transparent;
|
.logo h1 {
|
||||||
border-bottom: none;
|
color: #fff;
|
||||||
overflow-x: auto;
|
margin: 0;
|
||||||
white-space: nowrap;
|
font-size: 20px;
|
||||||
flex-wrap: nowrap;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
.el-menu-item {
|
|
||||||
|
.nav-menu {
|
||||||
|
flex: 1;
|
||||||
|
border: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-menu--horizontal {
|
||||||
|
border-bottom: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-menu--horizontal > .el-menu-item {
|
||||||
|
height: 60px;
|
||||||
|
line-height: 60px;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-weight: 500;
|
padding: 0 20px;
|
||||||
|
border-bottom: none !important;
|
||||||
}
|
}
|
||||||
@media screen and (max-width: 1024px) {
|
|
||||||
.el-header {
|
.el-header {
|
||||||
flex-direction: row;
|
padding: 0;
|
||||||
height: 50px;
|
background-color: #409EFF;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-menu--horizontal > .el-menu-item:hover,
|
||||||
|
.el-menu--horizontal > .el-menu-item:focus,
|
||||||
|
.el-menu--horizontal > .el-menu-item.is-active {
|
||||||
|
background-color: rgba(255, 255, 255, 0.1) !important;
|
||||||
|
border-bottom: 2px solid #fff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-main {
|
||||||
|
padding: 20px;
|
||||||
|
flex: 1;
|
||||||
|
background-color: #f5f7fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-footer {
|
||||||
|
padding: 20px;
|
||||||
|
background-color: #fff;
|
||||||
|
border-top: 1px solid #e4e7ed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-content {
|
||||||
|
text-align: center;
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式布局 */
|
||||||
|
@media screen and (max-width: 768px) {
|
||||||
|
.header-container {
|
||||||
padding: 0 10px;
|
padding: 0 10px;
|
||||||
}
|
}
|
||||||
.header-left h1 {
|
|
||||||
|
.logo {
|
||||||
|
margin-right: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo h1 {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
/* 不再隐藏el-menu */
|
|
||||||
}
|
.el-menu--horizontal > .el-menu-item {
|
||||||
.el-header .el-menu-item.is-active {
|
padding: 0 10px;
|
||||||
background-color: #fff !important;
|
font-size: 14px;
|
||||||
color: #409EFF !important;
|
}
|
||||||
border-radius: 8px 8px 0 0;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
.el-header .el-menu-item {
|
|
||||||
color: #fff !important;
|
|
||||||
transition: background 0.2s, color 0.2s;
|
|
||||||
}
|
|
||||||
.el-header .el-menu-item:not(.is-active):hover {
|
|
||||||
background: rgba(255,255,255,0.15) !important;
|
|
||||||
color: #fff !important;
|
|
||||||
}
|
|
||||||
.el-header .el-menu::-webkit-scrollbar {
|
|
||||||
height: 4px;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
83
frontend/src/api/advancedAnalysis.js
Normal file
83
frontend/src/api/advancedAnalysis.js
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
// 获取遗漏值分析
|
||||||
|
export function getMissingValueAnalysis(lotteryType, periods = 100) {
|
||||||
|
return request({
|
||||||
|
url: `/api/v1/advanced-analysis/missing-value/${lotteryType}`,
|
||||||
|
method: 'get',
|
||||||
|
params: { periods }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取和值分析
|
||||||
|
export function getSumValueAnalysis(lotteryType, periods = 100) {
|
||||||
|
return request({
|
||||||
|
url: `/api/v1/advanced-analysis/sum-value/${lotteryType}`,
|
||||||
|
method: 'get',
|
||||||
|
params: { periods }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取AC值分析
|
||||||
|
export function getAcValueAnalysis(lotteryType, periods = 100) {
|
||||||
|
return request({
|
||||||
|
url: `/api/v1/advanced-analysis/ac-value/${lotteryType}`,
|
||||||
|
method: 'get',
|
||||||
|
params: { periods }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取质合比分析
|
||||||
|
export function getPrimeCompositeAnalysis(lotteryType, periods = 100) {
|
||||||
|
return request({
|
||||||
|
url: `/api/v1/advanced-analysis/prime-composite/${lotteryType}`,
|
||||||
|
method: 'get',
|
||||||
|
params: { periods }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取012路分析
|
||||||
|
export function getRoad012Analysis(lotteryType, periods = 100) {
|
||||||
|
return request({
|
||||||
|
url: `/api/v1/advanced-analysis/road-012/${lotteryType}`,
|
||||||
|
method: 'get',
|
||||||
|
params: { periods }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取跨度分析
|
||||||
|
export function getSpanAnalysis(lotteryType, periods = 100) {
|
||||||
|
return request({
|
||||||
|
url: `/api/v1/advanced-analysis/span/${lotteryType}`,
|
||||||
|
method: 'get',
|
||||||
|
params: { periods }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取综合分析
|
||||||
|
export function getComprehensiveAnalysis(lotteryType, periods = 100) {
|
||||||
|
return request({
|
||||||
|
url: `/api/v1/advanced-analysis/comprehensive/${lotteryType}`,
|
||||||
|
method: 'get',
|
||||||
|
params: { periods }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 通用高级分析接口
|
||||||
|
export function getAdvancedAnalysis(lotteryType, analysisType, periods = 100) {
|
||||||
|
const urlMap = {
|
||||||
|
'missing': `/api/v1/advanced-analysis/missing-value/${lotteryType}`,
|
||||||
|
'sum': `/api/v1/advanced-analysis/sum-value/${lotteryType}`,
|
||||||
|
'ac': `/api/v1/advanced-analysis/ac-value/${lotteryType}`,
|
||||||
|
'prime': `/api/v1/advanced-analysis/prime-composite/${lotteryType}`,
|
||||||
|
'road': `/api/v1/advanced-analysis/road-012/${lotteryType}`,
|
||||||
|
'span': `/api/v1/advanced-analysis/span/${lotteryType}`,
|
||||||
|
'comprehensive': `/api/v1/advanced-analysis/comprehensive/${lotteryType}`
|
||||||
|
}
|
||||||
|
|
||||||
|
return request({
|
||||||
|
url: urlMap[analysisType],
|
||||||
|
method: 'get',
|
||||||
|
params: { periods }
|
||||||
|
})
|
||||||
|
}
|
||||||
37
frontend/src/api/prediction.js
Normal file
37
frontend/src/api/prediction.js
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
// 训练预测模型
|
||||||
|
export function trainPredictionModel(lotteryType, periods = 100) {
|
||||||
|
return request({
|
||||||
|
url: `/api/v1/prediction/train/${lotteryType}`,
|
||||||
|
method: 'post',
|
||||||
|
params: { periods }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 预测下一期号码
|
||||||
|
export function predictNextNumbers(lotteryType, periods = 10) {
|
||||||
|
return request({
|
||||||
|
url: `/api/v1/prediction/predict/${lotteryType}`,
|
||||||
|
method: 'get',
|
||||||
|
params: { periods }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 基于模式的预测
|
||||||
|
export function getPatternPrediction(lotteryType, periods = 100) {
|
||||||
|
return request({
|
||||||
|
url: `/api/v1/prediction/pattern/${lotteryType}`,
|
||||||
|
method: 'get',
|
||||||
|
params: { periods }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 集成预测
|
||||||
|
export function getEnsemblePrediction(lotteryType, periods = 100) {
|
||||||
|
return request({
|
||||||
|
url: `/api/v1/prediction/ensemble/${lotteryType}`,
|
||||||
|
method: 'get',
|
||||||
|
params: { periods }
|
||||||
|
})
|
||||||
|
}
|
||||||
@ -1,36 +1,51 @@
|
|||||||
import { createRouter, createWebHistory } from 'vue-router'
|
import { createRouter, createWebHistory } from 'vue-router'
|
||||||
|
import Home from '../views/Home.vue'
|
||||||
const routes = [
|
import SSQ from '../views/SSQ.vue'
|
||||||
{
|
import DLT from '../views/DLT.vue'
|
||||||
path: '/',
|
import Statistics from '../views/Statistics.vue'
|
||||||
name: 'Home',
|
import AdvancedAnalysis from '../views/AdvancedAnalysis.vue'
|
||||||
component: () => import('../views/Home.vue')
|
import Prediction from '../views/Prediction.vue'
|
||||||
},
|
import NumberGenerator from '../views/NumberGenerator.vue'
|
||||||
{
|
|
||||||
path: '/ssq',
|
|
||||||
name: 'SSQ',
|
|
||||||
component: () => import('../views/SSQ.vue')
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/dlt',
|
|
||||||
name: 'DLT',
|
|
||||||
component: () => import('../views/DLT.vue')
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/statistics',
|
|
||||||
name: 'Statistics',
|
|
||||||
component: () => import('../views/Statistics.vue')
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/number-generator',
|
|
||||||
name: 'NumberGenerator',
|
|
||||||
component: () => import('../views/NumberGenerator.vue')
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
history: createWebHistory(),
|
history: createWebHistory(),
|
||||||
routes
|
routes: [
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
name: 'Home',
|
||||||
|
component: Home
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/ssq',
|
||||||
|
name: 'SSQ',
|
||||||
|
component: SSQ
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/dlt',
|
||||||
|
name: 'DLT',
|
||||||
|
component: DLT
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/statistics',
|
||||||
|
name: 'Statistics',
|
||||||
|
component: Statistics
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/advanced-analysis',
|
||||||
|
name: 'AdvancedAnalysis',
|
||||||
|
component: AdvancedAnalysis
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/prediction',
|
||||||
|
name: 'Prediction',
|
||||||
|
component: Prediction
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/number-generator',
|
||||||
|
name: 'NumberGenerator',
|
||||||
|
component: NumberGenerator
|
||||||
|
}
|
||||||
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
export default router
|
export default router
|
||||||
576
frontend/src/views/AdvancedAnalysis.vue
Normal file
576
frontend/src/views/AdvancedAnalysis.vue
Normal file
@ -0,0 +1,576 @@
|
|||||||
|
<template>
|
||||||
|
<el-card class="advanced-analysis">
|
||||||
|
<template #header>
|
||||||
|
<el-form :inline="true" class="analysis-form" label-width="80px">
|
||||||
|
<el-form-item label="彩票类型">
|
||||||
|
<el-select v-model="lotteryType" placeholder="请选择" @change="loadAnalysis" style="width: 120px">
|
||||||
|
<el-option label="双色球" value="ssq"></el-option>
|
||||||
|
<el-option label="大乐透" value="dlt"></el-option>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="分析类型">
|
||||||
|
<el-select v-model="analysisType" placeholder="请选择" @change="loadAnalysis" style="width: 140px">
|
||||||
|
<el-option label="遗漏值分析" value="missing"></el-option>
|
||||||
|
<el-option label="和值分析" value="sum"></el-option>
|
||||||
|
<el-option label="AC值分析" value="ac"></el-option>
|
||||||
|
<el-option label="质合比分析" value="prime"></el-option>
|
||||||
|
<el-option label="012路分析" value="road"></el-option>
|
||||||
|
<el-option label="跨度分析" value="span"></el-option>
|
||||||
|
<el-option label="综合分析" value="comprehensive"></el-option>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="分析期数">
|
||||||
|
<el-input-number
|
||||||
|
v-model="periods"
|
||||||
|
:min="10"
|
||||||
|
:max="500"
|
||||||
|
style="width: 120px"
|
||||||
|
@change="loadAnalysis"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" @click="loadAnalysis" :loading="loading">
|
||||||
|
分析
|
||||||
|
</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="chart-area">
|
||||||
|
<div v-if="loading" class="loading">
|
||||||
|
<el-skeleton :rows="10" animated />
|
||||||
|
</div>
|
||||||
|
<div v-else-if="analysisData" class="analysis-content">
|
||||||
|
<div class="chart-title">
|
||||||
|
<h3>{{ analysisTypeLabelMap[analysisType] || '分析结果' }}</h3>
|
||||||
|
</div>
|
||||||
|
<div v-if="analysisType !== 'comprehensive'" class="chart-container">
|
||||||
|
<div ref="missingChart" v-show="analysisType === 'missing'" style="width: 100%; height: 400px;"></div>
|
||||||
|
<div ref="sumChart" v-show="analysisType === 'sum'" style="width: 100%; height: 400px;"></div>
|
||||||
|
<div ref="acChart" v-show="analysisType === 'ac'" style="width: 100%; height: 400px;"></div>
|
||||||
|
<div ref="primeChart" v-show="analysisType === 'prime'" style="width: 100%; height: 400px;"></div>
|
||||||
|
<div ref="roadChart" v-show="analysisType === 'road'" style="width: 100%; height: 400px;"></div>
|
||||||
|
<div ref="spanChart" v-show="analysisType === 'span'" style="width: 100%; height: 400px;"></div>
|
||||||
|
<el-empty v-if="!hasChart(analysisType)" description="暂无数据" />
|
||||||
|
</div>
|
||||||
|
<el-row :gutter="16" class="stat-cards" v-if="statItems.length">
|
||||||
|
<el-col v-for="item in statItems" :key="item.label" :span="6">
|
||||||
|
<el-card shadow="hover" class="stat-card">
|
||||||
|
<el-statistic :title="item.label" :value="item.value" />
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { ref, reactive, onMounted, nextTick, computed, watch } from 'vue'
|
||||||
|
import * as echarts from 'echarts'
|
||||||
|
import { getAdvancedAnalysis } from '@/api/advancedAnalysis'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'AdvancedAnalysis',
|
||||||
|
setup() {
|
||||||
|
const lotteryType = ref('ssq')
|
||||||
|
const analysisType = ref('missing')
|
||||||
|
const periods = ref(100)
|
||||||
|
const loading = ref(false)
|
||||||
|
const analysisData = ref(null)
|
||||||
|
const analysisTypeLabelMap = {
|
||||||
|
missing: '遗漏值分析',
|
||||||
|
sum: '和值分析',
|
||||||
|
ac: 'AC值分析',
|
||||||
|
prime: '质合比分析',
|
||||||
|
road: '012路分析',
|
||||||
|
span: '跨度分析',
|
||||||
|
comprehensive: '综合分析'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 图表引用
|
||||||
|
const missingChart = ref(null)
|
||||||
|
const sumChart = ref(null)
|
||||||
|
const acChart = ref(null)
|
||||||
|
const primeChart = ref(null)
|
||||||
|
const roadChart = ref(null)
|
||||||
|
const spanChart = ref(null)
|
||||||
|
const compMissingChart = ref(null)
|
||||||
|
const compSumChart = ref(null)
|
||||||
|
const compAcChart = ref(null)
|
||||||
|
const compSpanChart = ref(null)
|
||||||
|
|
||||||
|
// 图表实例
|
||||||
|
let chartInstances = {}
|
||||||
|
|
||||||
|
const statItems = computed(() => {
|
||||||
|
if (!analysisData.value) return []
|
||||||
|
if (analysisType.value === 'missing') {
|
||||||
|
return [
|
||||||
|
{ label: '最大遗漏值', value: analysisData.value.max_red_missing },
|
||||||
|
{ label: '平均遗漏值', value: Math.round(analysisData.value.avg_red_missing) },
|
||||||
|
{ label: '蓝球最大遗漏', value: analysisData.value.max_blue_missing },
|
||||||
|
{ label: '蓝球平均遗漏', value: Math.round(analysisData.value.avg_blue_missing) }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
if (analysisType.value === 'sum') {
|
||||||
|
return [
|
||||||
|
{ label: '最小和值', value: analysisData.value.min_sum },
|
||||||
|
{ label: '最大和值', value: analysisData.value.max_sum },
|
||||||
|
{ label: '平均和值', value: Math.round(analysisData.value.avg_sum) },
|
||||||
|
{ label: '中位数', value: Math.round(analysisData.value.median_sum) },
|
||||||
|
{ label: '标准差', value: Math.round(analysisData.value.std_sum) },
|
||||||
|
{ label: '分析期数', value: analysisData.value.sums.length }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
if (analysisType.value === 'ac') {
|
||||||
|
return [
|
||||||
|
{ label: '最小AC值', value: analysisData.value.min_ac },
|
||||||
|
{ label: '最大AC值', value: analysisData.value.max_ac },
|
||||||
|
{ label: '平均AC值', value: Math.round(analysisData.value.avg_ac) },
|
||||||
|
{ label: '中位数', value: Math.round(analysisData.value.median_ac) },
|
||||||
|
{ label: '标准差', value: Math.round(analysisData.value.std_ac) },
|
||||||
|
{ label: '分析期数', value: analysisData.value.ac_values.length }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
if (analysisType.value === 'span') {
|
||||||
|
return [
|
||||||
|
{ label: '最小跨度', value: analysisData.value.min_span },
|
||||||
|
{ label: '最大跨度', value: analysisData.value.max_span },
|
||||||
|
{ label: '平均跨度', value: Math.round(analysisData.value.avg_span) },
|
||||||
|
{ label: '中位数', value: Math.round(analysisData.value.median_span) },
|
||||||
|
{ label: '标准差', value: Math.round(analysisData.value.std_span) },
|
||||||
|
{ label: '分析期数', value: analysisData.value.spans.length }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
// 综合分析类型,展示所有维度的核心统计项
|
||||||
|
if (analysisType.value === 'comprehensive' && analysisData.value) {
|
||||||
|
const d = analysisData.value
|
||||||
|
return [
|
||||||
|
// 遗漏值
|
||||||
|
{ label: '最大遗漏值', value: d.missing_value.max_red_missing },
|
||||||
|
{ label: '平均遗漏值', value: Math.round(d.missing_value.avg_red_missing) },
|
||||||
|
{ label: '蓝球最大遗漏', value: d.missing_value.max_blue_missing },
|
||||||
|
{ label: '蓝球平均遗漏', value: Math.round(d.missing_value.avg_blue_missing) },
|
||||||
|
// 和值
|
||||||
|
{ label: '最小和值', value: d.sum_value.min_sum },
|
||||||
|
{ label: '最大和值', value: d.sum_value.max_sum },
|
||||||
|
{ label: '平均和值', value: Math.round(d.sum_value.avg_sum) },
|
||||||
|
// AC值
|
||||||
|
{ label: '最小AC值', value: d.ac_value.min_ac },
|
||||||
|
{ label: '最大AC值', value: d.ac_value.max_ac },
|
||||||
|
{ label: '平均AC值', value: Math.round(d.ac_value.avg_ac) },
|
||||||
|
// 跨度
|
||||||
|
{ label: '最小跨度', value: d.span.min_span },
|
||||||
|
{ label: '最大跨度', value: d.span.max_span },
|
||||||
|
{ label: '平均跨度', value: Math.round(d.span.avg_span) }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
return []
|
||||||
|
})
|
||||||
|
|
||||||
|
const hasChart = (type) => {
|
||||||
|
return ['missing', 'sum', 'ac', 'prime', 'road', 'span'].includes(type)
|
||||||
|
}
|
||||||
|
const getChartRef = (type) => {
|
||||||
|
switch (type) {
|
||||||
|
case 'missing': return 'missingChart'
|
||||||
|
case 'sum': return 'sumChart'
|
||||||
|
case 'ac': return 'acChart'
|
||||||
|
case 'prime': return 'primeChart'
|
||||||
|
case 'road': return 'roadChart'
|
||||||
|
case 'span': return 'spanChart'
|
||||||
|
default: return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadAnalysis = async () => {
|
||||||
|
if (!lotteryType.value || !analysisType.value) return
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const response = await getAdvancedAnalysis(
|
||||||
|
lotteryType.value,
|
||||||
|
analysisType.value,
|
||||||
|
periods.value
|
||||||
|
)
|
||||||
|
analysisData.value = response.data
|
||||||
|
await nextTick()
|
||||||
|
renderChart()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载分析数据失败:', error)
|
||||||
|
ElMessage.error('加载分析数据失败')
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderChart = () => {
|
||||||
|
if (!analysisData.value) return
|
||||||
|
nextTick(() => {
|
||||||
|
switch (analysisType.value) {
|
||||||
|
case 'missing': renderMissingChart(); break
|
||||||
|
case 'sum': renderSumChart(); break
|
||||||
|
case 'ac': renderAcChart(); break
|
||||||
|
case 'prime': renderPrimeChart(); break
|
||||||
|
case 'road': renderRoadChart(); break
|
||||||
|
case 'span': renderSpanChart(); break
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderMissingChart = () => {
|
||||||
|
if (!missingChart.value || !analysisData.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (chartInstances.missing) {
|
||||||
|
chartInstances.missing.dispose()
|
||||||
|
}
|
||||||
|
const chart = echarts.init(missingChart.value)
|
||||||
|
chartInstances.missing = chart
|
||||||
|
const redData = Object.entries(analysisData.value.red_missing || {})
|
||||||
|
.map(([num, missing]) => ({ value: missing, name: num }))
|
||||||
|
.sort((a, b) => Number(a.name) - Number(b.name))
|
||||||
|
const blueData = Object.entries(analysisData.value.blue_missing || {})
|
||||||
|
.map(([num, missing]) => ({ value: missing, name: num }))
|
||||||
|
.sort((a, b) => Number(a.name) - Number(b.name))
|
||||||
|
const option = {
|
||||||
|
title: {
|
||||||
|
text: '红球/蓝球遗漏值分布',
|
||||||
|
left: 'center',
|
||||||
|
top: 10,
|
||||||
|
textStyle: { fontSize: 16 }
|
||||||
|
},
|
||||||
|
tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } },
|
||||||
|
legend: { data: ['红球遗漏值', '蓝球遗漏值'], top: 40 },
|
||||||
|
grid: { left: '3%', right: '4%', bottom: '8%', containLabel: true },
|
||||||
|
xAxis: [{ type: 'category', data: redData.map(item => item.name), name: '号码', axisLabel: { rotate: 0 } }],
|
||||||
|
yAxis: [{ type: 'value', name: '遗漏期数' }],
|
||||||
|
series: [
|
||||||
|
{ name: '红球遗漏值', type: 'bar', data: redData.map(item => item.value), itemStyle: { color: '#ff4757' }, barWidth: '40%' },
|
||||||
|
{ name: '蓝球遗漏值', type: 'bar', data: blueData.map(item => item.value), itemStyle: { color: '#3742fa' }, barWidth: '40%' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
chart.setOption(option)
|
||||||
|
setTimeout(() => { chart.resize() }, 100)
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderSumChart = () => {
|
||||||
|
if (!sumChart.value) return
|
||||||
|
|
||||||
|
const chart = echarts.init(sumChart.value)
|
||||||
|
chartInstances.sum = chart
|
||||||
|
|
||||||
|
const sumDistribution = analysisData.value.sum_distribution
|
||||||
|
const data = Object.entries(sumDistribution)
|
||||||
|
.map(([sum, count]) => ({ value: count, name: sum }))
|
||||||
|
.sort((a, b) => parseInt(a.name) - parseInt(b.name))
|
||||||
|
|
||||||
|
const option = {
|
||||||
|
title: {
|
||||||
|
text: '和值分布分析',
|
||||||
|
left: 'center'
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'axis'
|
||||||
|
},
|
||||||
|
xAxis: {
|
||||||
|
type: 'category',
|
||||||
|
data: data.map(item => item.name),
|
||||||
|
name: '和值'
|
||||||
|
},
|
||||||
|
yAxis: {
|
||||||
|
type: 'value',
|
||||||
|
name: '出现次数'
|
||||||
|
},
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
type: 'line',
|
||||||
|
data: data.map(item => item.value),
|
||||||
|
smooth: true,
|
||||||
|
itemStyle: {
|
||||||
|
color: '#2ed573'
|
||||||
|
},
|
||||||
|
areaStyle: {
|
||||||
|
color: {
|
||||||
|
type: 'linear',
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
x2: 0,
|
||||||
|
y2: 1,
|
||||||
|
colorStops: [
|
||||||
|
{ offset: 0, color: 'rgba(46, 213, 115, 0.3)' },
|
||||||
|
{ offset: 1, color: 'rgba(46, 213, 115, 0.1)' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
chart.setOption(option)
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderAcChart = () => {
|
||||||
|
if (!acChart.value) return
|
||||||
|
|
||||||
|
const chart = echarts.init(acChart.value)
|
||||||
|
chartInstances.ac = chart
|
||||||
|
|
||||||
|
const acDistribution = analysisData.value.ac_distribution
|
||||||
|
const data = Object.entries(acDistribution)
|
||||||
|
.map(([ac, count]) => ({ value: count, name: ac }))
|
||||||
|
.sort((a, b) => parseInt(a.name) - parseInt(b.name))
|
||||||
|
|
||||||
|
const option = {
|
||||||
|
title: {
|
||||||
|
text: 'AC值分布分析',
|
||||||
|
left: 'center'
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'axis'
|
||||||
|
},
|
||||||
|
xAxis: {
|
||||||
|
type: 'category',
|
||||||
|
data: data.map(item => item.name),
|
||||||
|
name: 'AC值'
|
||||||
|
},
|
||||||
|
yAxis: {
|
||||||
|
type: 'value',
|
||||||
|
name: '出现次数'
|
||||||
|
},
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
type: 'bar',
|
||||||
|
data: data.map(item => item.value),
|
||||||
|
itemStyle: {
|
||||||
|
color: '#ffa502'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
chart.setOption(option)
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderPrimeChart = () => {
|
||||||
|
if (!primeChart.value) return
|
||||||
|
|
||||||
|
const chart = echarts.init(primeChart.value)
|
||||||
|
chartInstances.prime = chart
|
||||||
|
|
||||||
|
const data = analysisData.value.most_common_ratios.map(item => ({
|
||||||
|
value: item[1],
|
||||||
|
name: item[0]
|
||||||
|
}))
|
||||||
|
|
||||||
|
const option = {
|
||||||
|
title: {
|
||||||
|
text: '质合比分布',
|
||||||
|
left: 'center'
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'item',
|
||||||
|
formatter: '{a} <br/>{b}: {c} ({d}%)'
|
||||||
|
},
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
name: '质合比',
|
||||||
|
type: 'pie',
|
||||||
|
radius: '50%',
|
||||||
|
data: data,
|
||||||
|
emphasis: {
|
||||||
|
itemStyle: {
|
||||||
|
shadowBlur: 10,
|
||||||
|
shadowOffsetX: 0,
|
||||||
|
shadowColor: 'rgba(0, 0, 0, 0.5)'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
chart.setOption(option)
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderRoadChart = () => {
|
||||||
|
if (!roadChart.value) return
|
||||||
|
|
||||||
|
const chart = echarts.init(roadChart.value)
|
||||||
|
chartInstances.road = chart
|
||||||
|
|
||||||
|
const data = analysisData.value.most_common_patterns.map(item => ({
|
||||||
|
value: item[1],
|
||||||
|
name: item[0]
|
||||||
|
}))
|
||||||
|
|
||||||
|
const option = {
|
||||||
|
title: {
|
||||||
|
text: '012路分布',
|
||||||
|
left: 'center'
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'item',
|
||||||
|
formatter: '{a} <br/>{b}: {c} ({d}%)'
|
||||||
|
},
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
name: '012路',
|
||||||
|
type: 'pie',
|
||||||
|
radius: '50%',
|
||||||
|
data: data,
|
||||||
|
emphasis: {
|
||||||
|
itemStyle: {
|
||||||
|
shadowBlur: 10,
|
||||||
|
shadowOffsetX: 0,
|
||||||
|
shadowColor: 'rgba(0, 0, 0, 0.5)'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
chart.setOption(option)
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderSpanChart = () => {
|
||||||
|
if (!spanChart.value) return
|
||||||
|
|
||||||
|
const chart = echarts.init(spanChart.value)
|
||||||
|
chartInstances.span = chart
|
||||||
|
|
||||||
|
const spanDistribution = analysisData.value.span_distribution
|
||||||
|
const data = Object.entries(spanDistribution)
|
||||||
|
.map(([span, count]) => ({ value: count, name: span }))
|
||||||
|
.sort((a, b) => parseInt(a.name) - parseInt(b.name))
|
||||||
|
|
||||||
|
const option = {
|
||||||
|
title: {
|
||||||
|
text: '跨度分布分析',
|
||||||
|
left: 'center'
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'axis'
|
||||||
|
},
|
||||||
|
xAxis: {
|
||||||
|
type: 'category',
|
||||||
|
data: data.map(item => item.name),
|
||||||
|
name: '跨度'
|
||||||
|
},
|
||||||
|
yAxis: {
|
||||||
|
type: 'value',
|
||||||
|
name: '出现次数'
|
||||||
|
},
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
type: 'bar',
|
||||||
|
data: data.map(item => item.value),
|
||||||
|
itemStyle: {
|
||||||
|
color: '#ff6348'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
chart.setOption(option)
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
window.addEventListener('resize', () => {
|
||||||
|
Object.values(chartInstances).forEach(chart => {
|
||||||
|
if (chart && typeof chart.resize === 'function') {
|
||||||
|
chart.resize()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
// onMounted后主动渲染一次
|
||||||
|
nextTick(() => {
|
||||||
|
renderChart()
|
||||||
|
})
|
||||||
|
loadAnalysis()
|
||||||
|
})
|
||||||
|
|
||||||
|
// watch监听数据和类型变化,确保DOM和数据都准备好时再渲染
|
||||||
|
watch([analysisData, analysisType], async () => {
|
||||||
|
await nextTick()
|
||||||
|
renderChart()
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
lotteryType,
|
||||||
|
analysisType,
|
||||||
|
periods,
|
||||||
|
loading,
|
||||||
|
analysisData,
|
||||||
|
analysisTypeLabelMap,
|
||||||
|
missingChart,
|
||||||
|
sumChart,
|
||||||
|
acChart,
|
||||||
|
primeChart,
|
||||||
|
roadChart,
|
||||||
|
spanChart,
|
||||||
|
compMissingChart,
|
||||||
|
compSumChart,
|
||||||
|
compAcChart,
|
||||||
|
compSpanChart,
|
||||||
|
loadAnalysis,
|
||||||
|
hasChart,
|
||||||
|
getChartRef,
|
||||||
|
statItems
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.advanced-analysis {
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
.analysis-form {
|
||||||
|
margin-bottom: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 16px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.chart-area {
|
||||||
|
margin-top: 16px;
|
||||||
|
min-height: 480px;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 8px 0 rgba(64,158,255,0.04);
|
||||||
|
padding: 24px 16px 8px 16px;
|
||||||
|
}
|
||||||
|
.chart-title {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #409EFF;
|
||||||
|
}
|
||||||
|
.chart-container {
|
||||||
|
min-height: 400px;
|
||||||
|
background: #fafbfc;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid #ebeef5;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.stat-cards {
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
.stat-card {
|
||||||
|
text-align: center;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #f8f9fa;
|
||||||
|
box-shadow: 0 1px 4px 0 rgba(64,158,255,0.04);
|
||||||
|
}
|
||||||
|
.loading {
|
||||||
|
padding: 40px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.no-data {
|
||||||
|
padding: 60px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
604
frontend/src/views/Prediction.vue
Normal file
604
frontend/src/views/Prediction.vue
Normal file
@ -0,0 +1,604 @@
|
|||||||
|
<template>
|
||||||
|
<div class="prediction">
|
||||||
|
<el-card class="prediction-card">
|
||||||
|
<template #header>
|
||||||
|
<el-form :inline="true" class="prediction-form" label-width="80px">
|
||||||
|
<el-form-item label="彩票类型">
|
||||||
|
<el-select v-model="lotteryType" placeholder="请选择" @change="resetPrediction" style="width: 120px">
|
||||||
|
<el-option label="双色球" value="ssq"></el-option>
|
||||||
|
<el-option label="大乐透" value="dlt"></el-option>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="训练期数">
|
||||||
|
<el-input-number
|
||||||
|
v-model="trainingPeriods"
|
||||||
|
:min="50"
|
||||||
|
:max="500"
|
||||||
|
style="width: 120px"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" @click="trainModel" :loading="training">
|
||||||
|
训练模型
|
||||||
|
</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="prediction-content">
|
||||||
|
<!-- 预测结果优先展示 -->
|
||||||
|
<div v-if="predictionResults.length > 0" class="prediction-results">
|
||||||
|
<h3>预测结果</h3>
|
||||||
|
<el-tabs v-model="activeTab" type="border-card">
|
||||||
|
<el-tab-pane label="机器学习预测" name="ml">
|
||||||
|
<div v-if="mlPrediction" class="prediction-item">
|
||||||
|
<div class="prediction-header">
|
||||||
|
<h4>机器学习预测</h4>
|
||||||
|
<el-tag type="success">置信度: 高</el-tag>
|
||||||
|
</div>
|
||||||
|
<div class="prediction-numbers">
|
||||||
|
<div class="red-balls">
|
||||||
|
<span v-for="num in mlPrediction.predicted_numbers" :key="num" class="ball red-ball">
|
||||||
|
{{ num.toString().padStart(2, '0') }}
|
||||||
|
</span>
|
||||||
|
<span v-if="mlPrediction.predicted_blue" class="ball blue-ball">
|
||||||
|
{{ mlPrediction.predicted_blue.toString().padStart(2, '0') }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="prediction-info">
|
||||||
|
<p>{{ mlPrediction.confidence }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else class="no-prediction">暂无机器学习预测结果</div>
|
||||||
|
</el-tab-pane>
|
||||||
|
|
||||||
|
<el-tab-pane label="模式预测" name="pattern">
|
||||||
|
<div v-if="patternPrediction" class="prediction-item">
|
||||||
|
<div class="prediction-header">
|
||||||
|
<h4>模式分析预测</h4>
|
||||||
|
<el-tag type="warning">置信度: 中</el-tag>
|
||||||
|
</div>
|
||||||
|
<div v-if="patternPrediction.predicted_numbers" class="prediction-numbers">
|
||||||
|
<div class="red-balls">
|
||||||
|
<span v-for="num in patternPrediction.predicted_numbers" :key="num" class="ball red-ball">
|
||||||
|
{{ num.toString().padStart(2, '0') }}
|
||||||
|
</span>
|
||||||
|
<span v-if="patternPrediction.predicted_blue" class="ball blue-ball">
|
||||||
|
{{ patternPrediction.predicted_blue.toString().padStart(2, '0') }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="pattern-criteria">
|
||||||
|
<el-descriptions title="推荐选号标准" :column="2" border>
|
||||||
|
<el-descriptions-item label="和值范围">
|
||||||
|
{{ patternPrediction.suggested_criteria.sum_range.join(' - ') }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="奇偶比">
|
||||||
|
{{ patternPrediction.suggested_criteria.odd_even_ratio }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="分区数量">
|
||||||
|
{{ patternPrediction.suggested_criteria.zone_distribution }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="连号数量">
|
||||||
|
{{ patternPrediction.suggested_criteria.consecutive_count }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else class="no-prediction">暂无模式预测结果</div>
|
||||||
|
</el-tab-pane>
|
||||||
|
|
||||||
|
<el-tab-pane label="集成预测" name="ensemble">
|
||||||
|
<div v-if="ensemblePrediction" class="prediction-item">
|
||||||
|
<div class="prediction-header">
|
||||||
|
<h4>集成预测结果</h4>
|
||||||
|
<el-tag type="info">多方法综合</el-tag>
|
||||||
|
</div>
|
||||||
|
<div class="ensemble-recommendations">
|
||||||
|
<div v-for="(rec, index) in ensemblePrediction.recommendations" :key="index" class="recommendation">
|
||||||
|
<div class="recommendation-header">
|
||||||
|
<h5>{{ rec.method }}</h5>
|
||||||
|
<el-tag :type="rec.confidence === '高' ? 'success' : rec.confidence === '中' ? 'warning' : 'info'">
|
||||||
|
置信度: {{ rec.confidence }}
|
||||||
|
</el-tag>
|
||||||
|
</div>
|
||||||
|
<div class="recommendation-numbers">
|
||||||
|
<div class="red-balls">
|
||||||
|
<span v-for="num in rec.numbers" :key="num" class="ball red-ball">
|
||||||
|
{{ num.toString().padStart(2, '0') }}
|
||||||
|
</span>
|
||||||
|
<span v-if="rec.blue" class="ball blue-ball">
|
||||||
|
{{ rec.blue.toString().padStart(2, '0') }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="pattern-analysis">
|
||||||
|
<h5>模式分析</h5>
|
||||||
|
<el-descriptions :column="2" border>
|
||||||
|
<el-descriptions-item label="和值范围">
|
||||||
|
{{ ensemblePrediction.pattern_analysis.sum_range.join(' - ') }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="奇偶比">
|
||||||
|
{{ ensemblePrediction.pattern_analysis.odd_even_ratio }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="分区数量">
|
||||||
|
{{ ensemblePrediction.pattern_analysis.zone_distribution }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="连号数量">
|
||||||
|
{{ ensemblePrediction.pattern_analysis.consecutive_count }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else class="no-prediction">暂无集成预测结果</div>
|
||||||
|
</el-tab-pane>
|
||||||
|
</el-tabs>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 模型训练状态 -->
|
||||||
|
<el-alert
|
||||||
|
v-if="modelStatus"
|
||||||
|
:title="modelStatus.title"
|
||||||
|
:type="modelStatus.type"
|
||||||
|
:description="modelStatus.description"
|
||||||
|
show-icon
|
||||||
|
:closable="false"
|
||||||
|
class="model-status"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 预测操作按钮区域 -->
|
||||||
|
<div class="prediction-actions">
|
||||||
|
<el-row :gutter="20" justify="center" align="middle">
|
||||||
|
<el-col :span="6">
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
@click="predictML"
|
||||||
|
:loading="predicting"
|
||||||
|
block
|
||||||
|
>
|
||||||
|
{{ modelStatus && modelStatus.type !== 'error' ? '机器学习预测' : '训练并预测' }}
|
||||||
|
</el-button>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="6">
|
||||||
|
<el-button
|
||||||
|
type="warning"
|
||||||
|
@click="predictPattern"
|
||||||
|
:loading="predicting"
|
||||||
|
block
|
||||||
|
>
|
||||||
|
模式预测
|
||||||
|
</el-button>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="6">
|
||||||
|
<el-button
|
||||||
|
type="info"
|
||||||
|
@click="predictEnsemble"
|
||||||
|
:loading="predicting"
|
||||||
|
block
|
||||||
|
>
|
||||||
|
{{ modelStatus && modelStatus.type !== 'error' ? '集成预测' : '训练并预测' }}
|
||||||
|
</el-button>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 历史预测记录 -->
|
||||||
|
<div v-if="predictionHistory.length > 0" class="prediction-history">
|
||||||
|
<h3>历史预测记录</h3>
|
||||||
|
<el-table :data="predictionHistory" stripe>
|
||||||
|
<el-table-column prop="timestamp" label="预测时间" width="180" />
|
||||||
|
<el-table-column prop="lotteryType" label="彩票类型" width="100" />
|
||||||
|
<el-table-column prop="method" label="预测方法" width="120" />
|
||||||
|
<el-table-column prop="numbers" label="预测号码">
|
||||||
|
<template #default="scope">
|
||||||
|
<div class="prediction-numbers">
|
||||||
|
<div class="red-balls">
|
||||||
|
<span v-for="(num, index) in scope.row.numbers.split(' ')"
|
||||||
|
:key="index"
|
||||||
|
class="ball red-ball"
|
||||||
|
>
|
||||||
|
{{ num.padStart(2, '0') }}
|
||||||
|
</span>
|
||||||
|
<span v-if="scope.row.blue" class="ball blue-ball">
|
||||||
|
{{ scope.row.blue.toString().padStart(2, '0') }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="confidence" label="置信度" width="100">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-tag :type="scope.row.confidence === '高' ? 'success' : scope.row.confidence === '中' ? 'warning' : 'info'">
|
||||||
|
{{ scope.row.confidence }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { ref, watch, onMounted } from 'vue'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import {
|
||||||
|
trainPredictionModel,
|
||||||
|
predictNextNumbers,
|
||||||
|
getPatternPrediction,
|
||||||
|
getEnsemblePrediction
|
||||||
|
} from '@/api/prediction'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'Prediction',
|
||||||
|
setup() {
|
||||||
|
const lotteryType = ref('ssq')
|
||||||
|
const trainingPeriods = ref(100)
|
||||||
|
const training = ref(false)
|
||||||
|
const predicting = ref(false)
|
||||||
|
const activeTab = ref('ml')
|
||||||
|
|
||||||
|
const modelStatus = ref(null)
|
||||||
|
const mlPrediction = ref(null)
|
||||||
|
const patternPrediction = ref(null)
|
||||||
|
const ensemblePrediction = ref(null)
|
||||||
|
const predictionResults = ref([])
|
||||||
|
const predictionHistory = ref([])
|
||||||
|
|
||||||
|
const trainModel = async () => {
|
||||||
|
training.value = true
|
||||||
|
try {
|
||||||
|
const response = await trainPredictionModel(lotteryType.value, trainingPeriods.value)
|
||||||
|
|
||||||
|
if (response.data.success) {
|
||||||
|
modelStatus.value = {
|
||||||
|
type: 'success',
|
||||||
|
title: '模型训练成功',
|
||||||
|
description: `平均准确率: ${(response.data.avg_accuracy * 100).toFixed(2)}%, 训练样本: ${response.data.training_samples}`
|
||||||
|
}
|
||||||
|
ElMessage.success('模型训练成功')
|
||||||
|
} else {
|
||||||
|
modelStatus.value = {
|
||||||
|
type: 'error',
|
||||||
|
title: '模型训练失败',
|
||||||
|
description: response.data.message
|
||||||
|
}
|
||||||
|
ElMessage.error(response.data.message)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('训练模型失败:', error)
|
||||||
|
modelStatus.value = {
|
||||||
|
type: 'error',
|
||||||
|
title: '模型训练失败',
|
||||||
|
description: '网络错误或服务器异常'
|
||||||
|
}
|
||||||
|
ElMessage.error('训练模型失败')
|
||||||
|
} finally {
|
||||||
|
training.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const predictML = async () => {
|
||||||
|
predicting.value = true
|
||||||
|
try {
|
||||||
|
// 如果模型未训练,先训练模型
|
||||||
|
if (!modelStatus.value || modelStatus.value.type === 'error') {
|
||||||
|
const trainResult = await trainPredictionModel(lotteryType.value, trainingPeriods.value)
|
||||||
|
if (!trainResult.data.success) {
|
||||||
|
ElMessage.error('模型训练失败:' + trainResult.data.message)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
modelStatus.value = {
|
||||||
|
type: 'success',
|
||||||
|
title: '模型训练成功',
|
||||||
|
description: `平均准确率: ${(trainResult.data.avg_accuracy * 100).toFixed(2)}%, 训练样本: ${trainResult.data.training_samples}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 进行预测
|
||||||
|
const response = await predictNextNumbers(lotteryType.value, 10)
|
||||||
|
|
||||||
|
if (response.data.success) {
|
||||||
|
mlPrediction.value = response.data
|
||||||
|
activeTab.value = 'ml'
|
||||||
|
predictionResults.value = [response.data]
|
||||||
|
predictionHistory.value.unshift({
|
||||||
|
timestamp: new Date().toLocaleString(),
|
||||||
|
lotteryType: lotteryType.value === 'ssq' ? '双色球' : '大乐透',
|
||||||
|
method: '机器学习',
|
||||||
|
numbers: response.data.predicted_numbers.join(' '),
|
||||||
|
blue: response.data.predicted_blue,
|
||||||
|
confidence: '高'
|
||||||
|
})
|
||||||
|
ElMessage.success('机器学习预测完成')
|
||||||
|
} else {
|
||||||
|
mlPrediction.value = null
|
||||||
|
ElMessage.error(response.data.message)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
mlPrediction.value = null
|
||||||
|
console.error('机器学习预测失败:', error)
|
||||||
|
ElMessage.error('机器学习预测失败')
|
||||||
|
} finally {
|
||||||
|
predicting.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const predictPattern = async () => {
|
||||||
|
predicting.value = true
|
||||||
|
try {
|
||||||
|
const response = await getPatternPrediction(lotteryType.value, trainingPeriods.value)
|
||||||
|
|
||||||
|
if (response.data.success) {
|
||||||
|
patternPrediction.value = response.data
|
||||||
|
activeTab.value = 'pattern'
|
||||||
|
predictionResults.value = [response.data]
|
||||||
|
// 添加到历史记录
|
||||||
|
predictionHistory.value.unshift({
|
||||||
|
timestamp: new Date().toLocaleString(),
|
||||||
|
lotteryType: lotteryType.value === 'ssq' ? '双色球' : '大乐透',
|
||||||
|
method: '模式预测',
|
||||||
|
numbers: response.data.predicted_numbers.join(' '),
|
||||||
|
blue: response.data.predicted_blue,
|
||||||
|
confidence: '中'
|
||||||
|
})
|
||||||
|
ElMessage.success('模式预测完成')
|
||||||
|
} else {
|
||||||
|
patternPrediction.value = null
|
||||||
|
ElMessage.error(response.data.message)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
patternPrediction.value = null
|
||||||
|
console.error('模式预测失败:', error)
|
||||||
|
ElMessage.error('模式预测失败')
|
||||||
|
} finally {
|
||||||
|
predicting.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const predictEnsemble = async () => {
|
||||||
|
predicting.value = true
|
||||||
|
try {
|
||||||
|
// 如果模型未训练,先训练模型(因为集成预测也需要机器学习结果)
|
||||||
|
if (!modelStatus.value || modelStatus.value.type === 'error') {
|
||||||
|
const trainResult = await trainPredictionModel(lotteryType.value, trainingPeriods.value)
|
||||||
|
if (!trainResult.data.success) {
|
||||||
|
ElMessage.error('模型训练失败:' + trainResult.data.message)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
modelStatus.value = {
|
||||||
|
type: 'success',
|
||||||
|
title: '模型训练成功',
|
||||||
|
description: `平均准确率: ${(trainResult.data.avg_accuracy * 100).toFixed(2)}%, 训练样本: ${trainResult.data.training_samples}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await getEnsemblePrediction(lotteryType.value, trainingPeriods.value)
|
||||||
|
|
||||||
|
if (response.data.success) {
|
||||||
|
ensemblePrediction.value = response.data
|
||||||
|
activeTab.value = 'ensemble'
|
||||||
|
predictionResults.value = [response.data]
|
||||||
|
// 为每个推荐结果添加历史记录
|
||||||
|
response.data.recommendations.forEach(rec => {
|
||||||
|
predictionHistory.value.unshift({
|
||||||
|
timestamp: new Date().toLocaleString(),
|
||||||
|
lotteryType: lotteryType.value === 'ssq' ? '双色球' : '大乐透',
|
||||||
|
method: `集成预测-${rec.method}`,
|
||||||
|
numbers: rec.numbers.join(' '),
|
||||||
|
blue: rec.blue,
|
||||||
|
confidence: rec.confidence
|
||||||
|
})
|
||||||
|
})
|
||||||
|
ElMessage.success('集成预测完成')
|
||||||
|
} else {
|
||||||
|
ensemblePrediction.value = null
|
||||||
|
ElMessage.error(response.data.message)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
ensemblePrediction.value = null
|
||||||
|
console.error('集成预测失败:', error)
|
||||||
|
ElMessage.error('集成预测失败')
|
||||||
|
} finally {
|
||||||
|
predicting.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetPrediction = () => {
|
||||||
|
modelStatus.value = null
|
||||||
|
mlPrediction.value = null
|
||||||
|
patternPrediction.value = null
|
||||||
|
ensemblePrediction.value = null
|
||||||
|
predictionResults.value = []
|
||||||
|
// 自动训练新的模型
|
||||||
|
trainModel()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听彩票类型变化
|
||||||
|
watch(lotteryType, (newType) => {
|
||||||
|
if (newType) {
|
||||||
|
resetPrediction()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 组件挂载时自动训练模型
|
||||||
|
onMounted(() => {
|
||||||
|
trainModel()
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
lotteryType,
|
||||||
|
trainingPeriods,
|
||||||
|
training,
|
||||||
|
predicting,
|
||||||
|
activeTab,
|
||||||
|
modelStatus,
|
||||||
|
mlPrediction,
|
||||||
|
patternPrediction,
|
||||||
|
ensemblePrediction,
|
||||||
|
predictionResults,
|
||||||
|
predictionHistory,
|
||||||
|
trainModel,
|
||||||
|
predictML,
|
||||||
|
predictPattern,
|
||||||
|
predictEnsemble,
|
||||||
|
resetPrediction
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.prediction {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prediction-card {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prediction-form {
|
||||||
|
margin-bottom: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 16px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prediction-content {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.model-status {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prediction-results {
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prediction-item {
|
||||||
|
padding: 20px;
|
||||||
|
border: 1px solid #ebeef5;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prediction-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prediction-header h4 {
|
||||||
|
margin: 0;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prediction-numbers {
|
||||||
|
margin: 15px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.red-balls {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ball {
|
||||||
|
display: inline-block;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
line-height: 40px;
|
||||||
|
text-align: center;
|
||||||
|
border-radius: 50%;
|
||||||
|
font-weight: bold;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.red-ball {
|
||||||
|
background-color: #ff4757;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blue-ball {
|
||||||
|
background-color: #3742fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prediction-info {
|
||||||
|
color: #606266;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pattern-criteria {
|
||||||
|
margin: 15px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ensemble-recommendations {
|
||||||
|
margin: 15px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recommendation {
|
||||||
|
border: 1px solid #ebeef5;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 15px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recommendation-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recommendation-header h5 {
|
||||||
|
margin: 0;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recommendation-numbers {
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pattern-analysis {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pattern-analysis h5 {
|
||||||
|
color: #303133;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prediction-actions {
|
||||||
|
margin: 20px 0 0 0;
|
||||||
|
padding: 10px 0 0 0;
|
||||||
|
background: none;
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prediction-actions .el-row {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prediction-history {
|
||||||
|
margin-top: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-prediction {
|
||||||
|
color: #bbb;
|
||||||
|
text-align: center;
|
||||||
|
padding: 30px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
color: #303133;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
border-bottom: 2px solid #409eff;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Loading…
x
Reference in New Issue
Block a user