智能选号功能丰富,加入多种模式
This commit is contained in:
parent
a093b50d5a
commit
3e07f72b98
212
README.md
212
README.md
@ -8,8 +8,11 @@
|
||||
- Python 3.8+
|
||||
- FastAPI
|
||||
- SQLAlchemy
|
||||
- PostgreSQL
|
||||
- MySQL/PostgreSQL
|
||||
- Pydantic
|
||||
- Scikit-learn (机器学习)
|
||||
- NumPy (数值计算)
|
||||
- Pandas (数据处理)
|
||||
|
||||
### 前端
|
||||
- Vue 3
|
||||
@ -20,23 +23,42 @@
|
||||
- Vue Router
|
||||
|
||||
## 系统功能
|
||||
1. 数据管理
|
||||
- 双色球和大乐透历史数据导入
|
||||
- 数据查询和筛选
|
||||
- 数据导出
|
||||
- 手动数据录入
|
||||
|
||||
2. 统计分析
|
||||
- 号码出现频率统计
|
||||
- 热门号码分析
|
||||
- 冷门号码分析
|
||||
- 数据可视化展示
|
||||
### 1. 数据管理
|
||||
- 双色球和大乐透历史数据导入
|
||||
- 数据查询和筛选
|
||||
- 数据导出
|
||||
- 手动数据录入
|
||||
- 自动数据更新
|
||||
|
||||
3. 智能选号
|
||||
- 随机选号
|
||||
- 频率选号
|
||||
- 热门号码选号
|
||||
- 冷门号码选号
|
||||
### 2. 基础统计分析
|
||||
- 号码出现频率统计
|
||||
- 热门号码分析
|
||||
- 冷门号码分析
|
||||
- 数据可视化展示
|
||||
|
||||
### 3. 高级数据分析 ⭐ 新功能
|
||||
- **遗漏值分析**: 分析各号码的遗漏期数
|
||||
- **和值分析**: 统计红球和值的分布规律
|
||||
- **AC值分析**: 邻号差值分析
|
||||
- **质合比分析**: 质数与合数的比例分析
|
||||
- **012路分析**: 除3余数分析
|
||||
- **跨度分析**: 最大最小号码差值分析
|
||||
- **综合分析**: 多维度数据整合分析
|
||||
|
||||
### 4. 智能预测系统 ⭐ 新功能
|
||||
- **机器学习预测**: 基于历史数据的AI预测
|
||||
- **模式预测**: 基于统计模式的预测
|
||||
- **集成预测**: 多方法综合预测
|
||||
- **预测模型训练**: 可自定义训练参数
|
||||
- **预测结果评估**: 预测准确率统计
|
||||
|
||||
### 5. 智能选号
|
||||
- 随机选号
|
||||
- 频率选号
|
||||
- 热门号码选号
|
||||
- 冷门号码选号
|
||||
- 自定义选号策略
|
||||
|
||||
## 项目结构
|
||||
```
|
||||
@ -44,10 +66,15 @@ lottery/
|
||||
├── backend/ # 后端代码
|
||||
│ ├── app/
|
||||
│ │ ├── api/ # API 路由
|
||||
│ │ │ ├── endpoints/ # 基础API端点
|
||||
│ │ │ └── v1/ # API版本1
|
||||
│ │ ├── core/ # 核心配置
|
||||
│ │ ├── models/ # 数据模型
|
||||
│ │ ├── schemas/ # 数据验证
|
||||
│ │ └── services/ # 业务逻辑
|
||||
│ │ ├── analysis_service.py # 基础分析服务
|
||||
│ │ ├── advanced_analysis.py # 高级分析服务 ⭐
|
||||
│ │ └── prediction_service.py # 预测服务 ⭐
|
||||
│ ├── requirements.txt # 依赖包
|
||||
│ └── main.py # 入口文件
|
||||
├── frontend/ # 前端代码
|
||||
@ -57,6 +84,8 @@ lottery/
|
||||
│ │ ├── components/ # 组件
|
||||
│ │ ├── router/ # 路由配置
|
||||
│ │ ├── views/ # 页面
|
||||
│ │ │ ├── AdvancedAnalysis.vue # 高级分析页面 ⭐
|
||||
│ │ │ └── Prediction.vue # 预测页面 ⭐
|
||||
│ │ ├── App.vue # 根组件
|
||||
│ │ └── main.js # 入口文件
|
||||
│ ├── package.json # 依赖配置
|
||||
@ -67,7 +96,7 @@ lottery/
|
||||
## 开发环境要求
|
||||
- Python 3.8+
|
||||
- Node.js 16+
|
||||
- PostgreSQL 12+
|
||||
- MySQL 8.0+ 或 PostgreSQL 12+
|
||||
- npm 或 yarn
|
||||
|
||||
## 安装和运行
|
||||
@ -87,7 +116,7 @@ pip install -r requirements.txt
|
||||
```
|
||||
|
||||
3. 配置数据库
|
||||
- 创建 PostgreSQL 数据库
|
||||
- 创建 MySQL/PostgreSQL 数据库
|
||||
- 修改 `backend/app/core/database.py` 中的数据库连接配置
|
||||
|
||||
4. 启动服务
|
||||
@ -142,9 +171,30 @@ python schedule_update.py
|
||||
```
|
||||
系统会在每天凌晨2点自动检查并更新数据。
|
||||
|
||||
#### 其他说明
|
||||
- 首页、API、前端等所有"最新开奖"展示均以 `open_time` 最大值为准,保证数据准确。
|
||||
- 数据库不会因期号异常导致遗漏或重复,所有唯一性、顺序均以开奖日期为核心。
|
||||
### 高级分析功能 ⭐
|
||||
1. 进入"高级分析"页面
|
||||
2. 选择彩票类型(双色球/大乐透)
|
||||
3. 选择分析类型:
|
||||
- **遗漏值分析**: 查看各号码的遗漏期数
|
||||
- **和值分析**: 分析红球和值的分布规律
|
||||
- **AC值分析**: 邻号差值分析
|
||||
- **质合比分析**: 质数与合数比例
|
||||
- **012路分析**: 除3余数分布
|
||||
- **跨度分析**: 号码跨度统计
|
||||
- **综合分析**: 多维度整合分析
|
||||
4. 设置分析期数(10-500期)
|
||||
5. 点击"分析"按钮查看结果
|
||||
|
||||
### 智能预测功能 ⭐
|
||||
1. 进入"智能预测"页面
|
||||
2. 选择彩票类型
|
||||
3. 设置训练期数(建议100-500期)
|
||||
4. 点击"训练模型"按钮
|
||||
5. 训练完成后,可选择以下预测方法:
|
||||
- **机器学习预测**: 基于AI算法的预测
|
||||
- **模式预测**: 基于统计模式的预测
|
||||
- **集成预测**: 多方法综合预测
|
||||
6. 查看预测结果和置信度
|
||||
|
||||
### 数据查询
|
||||
1. 在查询表单中输入查询条件
|
||||
@ -167,6 +217,23 @@ python schedule_update.py
|
||||
- Swagger UI: http://localhost:8000/docs
|
||||
- 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. 数据库连接失败
|
||||
- 检查数据库服务是否启动
|
||||
@ -180,12 +247,21 @@ python schedule_update.py
|
||||
- 检查 JSON 文件格式是否正确
|
||||
- 确认数据库表结构是否完整
|
||||
|
||||
4. 机器学习预测失败
|
||||
- 确保历史数据充足(建议至少100期)
|
||||
- 检查训练参数设置是否合理
|
||||
|
||||
## 开发计划
|
||||
- [x] 添加高级数据分析功能
|
||||
- [x] 实现机器学习预测系统
|
||||
- [x] 优化数据可视化展示
|
||||
- [ ] 添加用户认证功能
|
||||
- [ ] 实现数据备份和恢复
|
||||
- [ ] 优化数据导入性能
|
||||
- [ ] 添加更多统计分析功能
|
||||
- [ ] 实现自定义选号策略
|
||||
- [ ] 添加移动端适配
|
||||
- [ ] 实现数据导出功能增强
|
||||
|
||||
## 贡献指南
|
||||
1. Fork 项目
|
||||
@ -197,84 +273,20 @@ python schedule_update.py
|
||||
## 许可证
|
||||
MIT License
|
||||
|
||||
## 数据更新功能
|
||||
## 更新日志
|
||||
|
||||
系统支持自动从聚合数据API获取最新的开奖数据并更新到本地数据库。更新功能包括:
|
||||
### v2.0.0 (2024-01-XX)
|
||||
- ✨ 新增高级数据分析功能
|
||||
- ✨ 新增机器学习预测系统
|
||||
- ✨ 新增遗漏值、和值、AC值等分析
|
||||
- ✨ 新增质合比、012路、跨度分析
|
||||
- ✨ 优化数据可视化展示
|
||||
- 🔧 更新依赖包版本
|
||||
- 📝 完善API文档
|
||||
|
||||
1. 手动更新
|
||||
```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
|
||||
```
|
||||
### v1.0.0 (2024-01-XX)
|
||||
- 🎉 初始版本发布
|
||||
- ✨ 基础数据管理功能
|
||||
- ✨ 基础统计分析
|
||||
- ✨ 智能选号功能
|
||||
- ✨ 前后端分离架构
|
||||
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.api.v1.lottery import router as lottery_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
|
||||
|
||||
# 创建数据库表
|
||||
@ -27,6 +29,10 @@ app.include_router(
|
||||
lottery_router, prefix=f"{settings.API_V1_STR}/lottery", tags=["lottery"])
|
||||
app.include_router(
|
||||
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__":
|
||||
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
|
||||
aiofiles==23.2.1
|
||||
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>
|
||||
<el-container class="app-container">
|
||||
<el-container class="layout-container">
|
||||
<el-header>
|
||||
<div class="header-left">
|
||||
<h1>彩票数据分析系统</h1>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<div class="header-container">
|
||||
<div class="logo">
|
||||
<h1>彩票数据分析系统</h1>
|
||||
</div>
|
||||
<el-menu
|
||||
:default-active="activeIndex"
|
||||
class="nav-menu"
|
||||
mode="horizontal"
|
||||
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="/ssq">双色球</el-menu-item>
|
||||
<el-menu-item index="/dlt">大乐透</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>
|
||||
</div>
|
||||
</el-header>
|
||||
|
||||
<el-main>
|
||||
<router-view></router-view>
|
||||
</el-main>
|
||||
|
||||
<el-footer>
|
||||
<div class="footer-content">
|
||||
<p>彩票数据分析系统 © 2024</p>
|
||||
</div>
|
||||
</el-footer>
|
||||
</el-container>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { ElContainer, ElHeader, ElMain, ElMenu, ElMenuItem, ElDrawer, ElButton } from 'element-plus'
|
||||
import { Menu } from '@element-plus/icons-vue'
|
||||
import { ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const drawer = ref(false)
|
||||
const isMobile = ref(false)
|
||||
const $route = useRoute()
|
||||
const router = useRouter()
|
||||
const activeIndex = ref('/')
|
||||
|
||||
const checkMobile = () => {
|
||||
isMobile.value = window.innerWidth <= 768
|
||||
const handleSelect = (key) => {
|
||||
activeIndex.value = key
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
checkMobile()
|
||||
window.addEventListener('resize', checkMobile)
|
||||
})
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', checkMobile)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.app-container {
|
||||
<style>
|
||||
.layout-container {
|
||||
min-height: 100vh;
|
||||
}
|
||||
.el-header {
|
||||
background-color: #409EFF;
|
||||
color: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 20px;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.header-container {
|
||||
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;
|
||||
align-items: center;
|
||||
padding: 0 20px;
|
||||
}
|
||||
.menu-btn {
|
||||
color: white;
|
||||
border: none;
|
||||
background: transparent;
|
||||
font-size: 22px;
|
||||
|
||||
.logo {
|
||||
margin-right: 40px;
|
||||
}
|
||||
.el-menu {
|
||||
background: transparent;
|
||||
border-bottom: none;
|
||||
overflow-x: auto;
|
||||
white-space: nowrap;
|
||||
flex-wrap: nowrap;
|
||||
|
||||
.logo h1 {
|
||||
color: #fff;
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
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-weight: 500;
|
||||
padding: 0 20px;
|
||||
border-bottom: none !important;
|
||||
}
|
||||
@media screen and (max-width: 1024px) {
|
||||
.el-header {
|
||||
flex-direction: row;
|
||||
height: 50px;
|
||||
|
||||
.el-header {
|
||||
padding: 0;
|
||||
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;
|
||||
}
|
||||
.header-left h1 {
|
||||
|
||||
.logo {
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
.logo h1 {
|
||||
font-size: 16px;
|
||||
}
|
||||
/* 不再隐藏el-menu */
|
||||
}
|
||||
.el-header .el-menu-item.is-active {
|
||||
background-color: #fff !important;
|
||||
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;
|
||||
|
||||
.el-menu--horizontal > .el-menu-item {
|
||||
padding: 0 10px;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
</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'
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: '/',
|
||||
name: 'Home',
|
||||
component: () => import('../views/Home.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')
|
||||
}
|
||||
]
|
||||
import Home from '../views/Home.vue'
|
||||
import SSQ from '../views/SSQ.vue'
|
||||
import DLT from '../views/DLT.vue'
|
||||
import Statistics from '../views/Statistics.vue'
|
||||
import AdvancedAnalysis from '../views/AdvancedAnalysis.vue'
|
||||
import Prediction from '../views/Prediction.vue'
|
||||
import NumberGenerator from '../views/NumberGenerator.vue'
|
||||
|
||||
const router = createRouter({
|
||||
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
|
||||
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