找回密码
 立即注册

QQ登录

只需一步,快速开始

搜索
热搜: 文档 工具 设计
查看: 7|回复: 0

【源码】双色球预测系统-多维度神经网络模型。。。美化

[复制链接]

2万

主题

1462

回帖

3万

积分

超级版主

教育辅助界扛把子

附加身份标识
精华
1
热心
10
听众
1
威望
7
贡献
16312
违规
0
书币
54975
注册时间
2020-4-8

论坛元老灌水之王

发表于 2025-5-19 18:25 | 显示全部楼层 |阅读模式
一、软件概述
双色球预测系统是一款基于深度学习技术的彩票预测工具,采用多种神经网络模型对历史开奖数据进行分析和预测。软件提供了直观美观的图形界面,支持数据获取、统计分析和结果预测三大核心功能。
二、主要功能
  • 自动获取最新双色球历史开奖数据
  • 红蓝球历史频率分析和气泡图可视化
  • 多种神经网络预测模型(LSTM、GRU、混合模型)
  • 参数可调的模型训练选项
  • 美观直观的预测结果展示
三、技术特点
  • 使用PyQt5构建美观响应式界面
  • 基于TensorFlow深度学习框架
  • 使用QWebEngineView实现高级HTML渲染效果
  • 多维度数据分析和特征工程
  • 动态加载动画和实时进度显示
四、安装使用
  • 解压"双色球预测系统_v1.0.0.zip"到任意位置
  • 双击文件夹中的"双色球预测系统.exe"即可运行
  • 首次使用请先获取历史数据
五、开发与贡献
  • 软件开发:Killerzeno
  • 界面美化:nobiyou

QQ截图20250519182236.png QQ截图20250519182304.png



源码:
[AppleScript] 纯文本查看 复制代码
import sys
import os
import requests
import json
import pandas as pd
import numpy as np
import tensorflow as tf
from tensorflow.keras.models import Sequential, load_model
from tensorflow.keras.layers import (Dense, LSTM, Dropout, BatchNormalization,
                                     Bidirectional, GRU, Conv1D, MaxPooling1D,
                                     Flatten, Layer, Input)
from tensorflow.keras.regularizers import l2
from tensorflow.keras.optimizers import Adam
from tensorflow.keras.callbacks import EarlyStopping, ModelCheckpoint
from tensorflow.keras.utils import to_categorical
from sklearn.preprocessing import MinMaxScaler
from sklearn.model_selection import train_test_split
from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout,
                             QHBoxLayout, QLabel, QPushButton,
                             QTextEdit, QSpinBox, QDoubleSpinBox, QProgressBar,
                             QMessageBox, QTabWidget, QGroupBox, QFormLayout, QComboBox)
from PyQt5.QtCore import QThread, pyqtSignal, Qt, QUrl, QObject, pyqtSlot
from PyQt5.QtGui import QIcon
from PyQt5.QtWebEngineWidgets import QWebEngineView, QWebEnginePage
from PyQt5.QtWebChannel import QWebChannel
import matplotlib

matplotlib.use('Qt5Agg')
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.figure import Figure
import matplotlib.pyplot as plt
from collections import Counter

# 导入气泡图HTML生成函数
from bubbles import generate_bubble_chart_html

def resource_path(relative_path):
    """ 解决打包后资源文件路径问题 """
    if hasattr(sys, '_MEIPASS'):
        return os.path.join(sys._MEIPASS, relative_path)
    return os.path.join(os.path.abspath("."), relative_path)

class AttentionLayer(Layer):
    def __init__(self, **kwargs):
        super(AttentionLayer, self).__init__(**kwargs)

    def build(self, input_shape):
        self.W = self.add_weight(name='attention_weight',
                                 shape=(input_shape[-1], 1),
                                 initializer='random_normal',
                                 trainable=True)
        self.b = self.add_weight(name='attention_bias',
                                 shape=(input_shape[1], 1),
                                 initializer='zeros',
                                 trainable=True)
        super(AttentionLayer, self).build(input_shape)

    def call(self, x):
        e = tf.tanh(tf.matmul(x, self.W) + self.b)
        a = tf.nn.softmax(e, axis=1)
        output = x * a
        return tf.reduce_sum(output, axis=1)

def is_prime(n):
    """判断是否为质数"""
    if n <= 1:
        return False
    for i in range(2, int(n ** 0.5) + 1):
        if n % i == 0:
            return False
    return True

class DataFetcher(QThread):
    finished = pyqtSignal(str)
    progress = pyqtSignal(int)

    def run(self):
        try:
            url = 'http://www.cwl.gov.cn/cwl_admin/front/cwlkj/search/kjxx/findDrawNotice'
            params = {
                'name': 'ssq',
                'issueCount': '',
                'issueStart': '',
                'issueEnd': '',
                'dayStart': '',
                'dayEnd': '',
                'pageNo': '1',
                'pageSize': '9999',
                'week': '',
                'systemType': 'PC'
            }
            response = requests.get(url, params=params)
            jsondata = response.json()

            if jsondata['state'] == 0:
                data = []
                total = len(jsondata['result'])

                # 收集所有的日期字符串,用于日志记录
                all_dates = []

                for i, item in enumerate(jsondata['result']):
                    # 获取日期字符串
                    date_str = item['date']
                    all_dates.append(date_str)

                    # 更严格地处理日期格式
                    try:
                        # 首先尝试分离掉括号部分(如果有)
                        date_str = date_str.split('(')[0].strip()
                        # 移除日期字符串中的非法字符
                        clean_date = ''.join([c for c in date_str if c.isdigit() or c == '-'])
                        # 确保日期格式为YYYY-MM-DD并且长度正确
                        if len(clean_date) >= 10:
                            clean_date = clean_date[:10]
                        else:
                            # 对于格式不正确的日期,使用期号的前8位作为替代(如果可能)
                            code = item['code']
                            if len(code) >= 8:
                                year = code[:4]
                                month = code[4:6]
                                day = code[6:8]
                                clean_date = f"{year}-{month}-{day}"
                            else:
                                # 最后的备选方案
                                clean_date = "2020-01-01"  # 使用占位符
                    except Exception as e:
                        print(f"处理日期时出错: {str(e)}, 原始日期: {date_str}")
                        clean_date = "2020-01-01"  # 使用占位符

                    # 获取蓝球和红球
                    try:
                        blue_ball = int(item['blue'])
                        red_balls = [int(rb) for rb in item['red'].split(',')]

                        # 确保有足够的红球
                        while len(red_balls) < 6:
                            red_balls.append(1)  # 使用1作为占位符

                        # 添加到数据列表
                        data.append([item['code'], clean_date, 
                                    red_balls[0], red_balls[1], red_balls[2],
                                    red_balls[3], red_balls[4], red_balls[5], 
                                    blue_ball])
                    except Exception as e:
                        print(f"处理球号时出错: {str(e)}, 期号: {item['code']}")
                        # 跳过这条数据
                        continue

                    self.progress.emit(int((i + 1) / total * 100))

                # 记录日期收集情况
                print(f"收集到 {len(all_dates)} 个日期")
                if len(all_dates) > 0:
                    print(f"示例日期: {all_dates[0]}")

                df = pd.DataFrame(data, columns=['期号', '日期', 'red1', 'red2', 'red3',
                                                 'red4', 'red5', 'red6', 'blue'])

                # 检查数据有效性
                print(f"数据总行数: {len(df)}")
                print(f"日期列类型: {df['日期'].dtype}")
                print(f"缺失值数量: {df.isna().sum().sum()}")

                # 保存前排序
                try:
                    df['temp_date'] = pd.to_datetime(df['日期'], errors='coerce')
                    df = df.sort_values(by='temp_date', ascending=False)
                    df.drop('temp_date', axis=1, inplace=True)
                except Exception as e:
                    print(f"排序数据时出错: {str(e)}")

                df.to_csv('data.csv', index=False, encoding='utf-8-sig')
                self.finished.emit(f"数据获取成功!共获取{len(data)}期数据。")
            else:
                self.finished.emit("数据获取失败:服务器返回错误状态。")
        except Exception as e:
            import traceback
            error_details = traceback.format_exc()
            self.finished.emit(f"数据获取失败:{str(e)}\n\n详细错误信息:\n{error_details}")

class EnhancedPredictor(QThread):
    finished = pyqtSignal(str)
    progress = pyqtSignal(int)
    stats_ready = pyqtSignal(dict)
    model_trained = pyqtSignal(object)

    def __init__(self, train_ratio, epochs, batch_size, lookback, strategy):
        super().__init__()
        self.train_ratio = train_ratio
        self.epochs = epochs
        self.batch_size = batch_size
        self.lookback = lookback
        self.strategy = strategy.lower()
        self.red_stats = None
        self.blue_stats = None
        self.scaler = None
        self.model = None

    def get_model_type(self):
        """获取当前使用的模型类型"""
        if self.strategy == 'lstm':
            return "多层LSTM神经网络"
        elif self.strategy == 'gru':
            return "双向GRU神经网络"
        else:
            return "混合模型(CNN+LSTM+GRU+Attention)"

    def get_model_architecture(self):
        """获取模型架构描述"""
        if self.strategy == 'lstm':
            return "LSTM(512)→LSTM(256)→Dense(128)"
        elif self.strategy == 'gru':
            return "BiGRU(256)→GRU(128)→Dense(64)"
        else:
            return "Conv1D→BiLSTM→GRU→Attention→Dense"

    def calculate_enhanced_stats(self, data):
        """增强的统计分析"""
        try:
            stats = {}

            # 确保数据是数值型
            for i in range(1, 7):
                if f'red{i}' in data.columns:
                    data[f'red{i}'] = pd.to_numeric(data[f'red{i}'], errors='coerce')
            if 'blue' in data.columns:
                data['blue'] = pd.to_numeric(data['blue'], errors='coerce')

            # 填充可能的NaN值
            for i in range(1, 7):
                if f'red{i}' in data.columns:
                    data[f'red{i}'].fillna(data[f'red{i}'].median(), inplace=True)
            if 'blue' in data.columns:
                data['blue'].fillna(data['blue'].median(), inplace=True)

            # 红球分析
            red_balls = []
            for i in range(1, 7):
                if f'red{i}' in data.columns:
                    red_balls.extend(data[f'red{i}'].values)

            # 计算所有红球出现次数
            red_counts = pd.Series(red_balls).value_counts().sort_index()
            red_probs = (red_counts / red_counts.sum()).sort_values(ascending=False)

            # 近期分析(最近100期)
            recent_data = data.tail(min(100, len(data)))
            recent_red = []
            for i in range(1, 7):
                if f'red{i}' in recent_data.columns:
                    recent_red.extend(recent_data[f'red{i}'].values)

            recent_red_counts = pd.Series(recent_red).value_counts()
            recent_red_probs = (recent_red_counts / recent_red_counts.sum()).sort_values(ascending=False)

            # 蓝球分析
            if 'blue' in data.columns:
                blue_counts = data['blue'].value_counts().sort_index()
                blue_probs = (blue_counts / blue_counts.sum()).sort_values(ascending=False)

                recent_blue_counts = recent_data['blue'].value_counts()
                recent_blue_probs = (recent_blue_counts / recent_blue_counts.sum()).sort_values(ascending=False)
            else:
                # 创建默认值
                blue_probs = pd.Series([1/16]*16, index=range(1, 17)).sort_values(ascending=False)
                recent_blue_probs = blue_probs.copy()

            stats['red'] = {
                'all_time_top10': dict(list(red_probs.head(10).items())),
                'recent_top10': dict(list(recent_red_probs.head(10).items())),
                'all_time_sorted': dict(red_probs.sort_index())
            }

            stats['blue'] = {
                'all_time_top10': dict(list(blue_probs.head(10).items())),
                'recent_top10': dict(list(recent_blue_probs.head(10).items())),
                'all_time_sorted': dict(blue_probs.sort_index())
            }

            return stats

        except Exception as e:
            print(f"统计分析出错: {str(e)}")
            # 创建一个默认统计结果
            default_stats = {
                'red': {
                    'all_time_top10': {i: 1/33 for i in range(1, 11)},
                    'recent_top10': {i: 1/33 for i in range(1, 11)},
                    'all_time_sorted': {i: 1/33 for i in range(1, 34)}
                },
                'blue': {
                    'all_time_top10': {i: 1/16 for i in range(1, 11)},
                    'recent_top10': {i: 1/16 for i in range(1, 11)},
                    'all_time_sorted': {i: 1/16 for i in range(1, 17)}
                }
            }
            return default_stats

    def create_lstm_model(self, input_shape):
        """创建LSTM模型"""
        model = Sequential([
            Input(shape=input_shape),
            LSTM(512, return_sequences=True,
                 kernel_regularizer=l2(0.01), recurrent_regularizer=l2(0.01),
                 dropout=0.2, recurrent_dropout=0.2),
            BatchNormalization(),
            Bidirectional(LSTM(256, return_sequences=True)),
            BatchNormalization(),
            Dropout(0.3),
            LSTM(128),
            BatchNormalization(),
            Dropout(0.3),
            Dense(128, activation='relu', kernel_regularizer=l2(0.01)),
            Dense(64, activation='relu'),
            Dense(7, activation='sigmoid')
        ])
        model.compile(loss='mean_squared_error',
                      optimizer=Adam(learning_rate=0.001),
                      metrics=['mae'])
        return model

    def create_gru_model(self, input_shape):
        """创建GRU模型"""
        model = Sequential([
            Input(shape=input_shape),
            Bidirectional(GRU(256, return_sequences=True,
                              kernel_regularizer=l2(0.01),
                              recurrent_regularizer=l2(0.01),
                              dropout=0.2,
                              recurrent_dropout=0.2)),
            BatchNormalization(),
            GRU(128, return_sequences=True),
            BatchNormalization(),
            Dropout(0.3),
            GRU(64),
            BatchNormalization(),
            Dropout(0.3),
            Dense(64, activation='relu'),
            Dense(32, activation='relu'),
            Dense(7, activation='sigmoid')
        ])
        model.compile(loss='mean_squared_error',
                      optimizer=Adam(learning_rate=0.001),
                      metrics=['mae'])
        return model

    def create_hybrid_model(self, input_shape):
        """创建混合模型"""
        model = Sequential([
            Input(shape=input_shape),
            Conv1D(64, 3, activation='relu', padding='same'),
            MaxPooling1D(2),
            BatchNormalization(),
            Bidirectional(LSTM(256, return_sequences=True)),
            BatchNormalization(),
            GRU(128, return_sequences=True),
            AttentionLayer(),
            BatchNormalization(),
            Dropout(0.4),
            Dense(128, activation='relu'),
            Dense(64, activation='relu'),
            Dense(7, activation='sigmoid')
        ])
        model.compile(loss='mean_squared_error',
                      optimizer=Adam(learning_rate=0.001),
                      metrics=['mae'])
        return model

    def fix_date_column(self, data):
        """修复日期列,确保它不会被转换为数值型"""
        # 创建一个新的日期列,避免破坏原始数据
        # 首先确保日期列是字符串类型
        data['日期'] = data['日期'].astype(str)

        # 检查日期格式并纠正
        valid_dates = []
        for date_str in data['日期']:
            # 清理日期字符串,只保留数字和连字符
            clean_date = ''.join([c for c in date_str if c.isdigit() or c == '-'])
            # 确保日期格式是YYYY-MM-DD
            if len(clean_date) >= 10:
                clean_date = clean_date[:10]  # 只取前10个字符
                valid_dates.append(clean_date)
            else:
                # 无效日期使用占位符
                valid_dates.append('2000-01-01')  # 使用占位符

        # 用清理后的日期创建新列
        data['clean_date'] = valid_dates

        try:
            # 转换为日期时间格式
            data['date'] = pd.to_datetime(data['clean_date'], errors='coerce')
            # 检查是否有无效日期
            invalid_dates = data['date'].isna().sum()
            if invalid_dates > 0:
                print(f"警告: 发现{invalid_dates}个无效日期,已替换为NaT")
        except Exception as e:
            print(f"日期转换异常: {str(e)}")
            # 创建一个假的日期序列
            data['date'] = pd.date_range(start='2020-01-01', periods=len(data))
            print("使用生成的日期序列代替原始日期")

        return data

    def prepare_data(self, data):
        """数据预处理和特征工程"""
        # 如果没有date列,先修复日期
        if 'date' not in data.columns:
            data = self.fix_date_column(data)

        # 添加时间特征
        try:
            data['year'] = data['date'].dt.year
            data['month'] = data['date'].dt.month
            data['day'] = data['date'].dt.day
            data['day_of_week'] = data['date'].dt.dayofweek
            data['day_of_year'] = data['date'].dt.dayofyear
        except Exception as e:
            print(f"时间特征提取错误: {str(e)}")
            # 创建默认时间特征
            data['year'] = 2020
            data['month'] = 1
            data['day'] = 1
            data['day_of_week'] = 0
            data['day_of_year'] = 1

        # 明确排除非数值列
        non_numeric_cols = ['期号', '日期', 'date', 'clean_date']

        # 添加统计特征
        for i in range(1, 7):
            data[f'red{i}_rolling_mean_10'] = data[f'red{i}'].rolling(10).mean()
            data[f'red{i}_rolling_std_10'] = data[f'red{i}'].rolling(10).std()

        # 添加组合特征
        data['red_sum'] = data[[f'red{i}' for i in range(1, 7)]].sum(axis=1)
        data['red_odd_count'] = data[[f'red{i}' for i in range(1, 7)]].apply(lambda x: x % 2).sum(axis=1)
        data['red_prime_count'] = data[[f'red{i}' for i in range(1, 7)]].apply(lambda x: x.apply(is_prime)).sum(axis=1)

        # 添加滞后特征
        for lag in [1, 2, 3, 5, 10]:
            for i in range(1, 7):
                data[f'red{i}_lag{lag}'] = data[f'red{i}'].shift(lag)
            data[f'blue_lag{lag}'] = data['blue'].shift(lag)

        # 确保所有特征值为数值型并填充NaN值
        numeric_cols = [col for col in data.columns if col not in non_numeric_cols]

        # 转换为数值类型并处理错误
        for col in numeric_cols:
            try:
                data[col] = pd.to_numeric(data[col], errors='coerce')
            except Exception as e:
                print(f"无法将列 {col} 转换为数值型: {str(e)}")

        # 填充NaN值
        data[numeric_cols] = data[numeric_cols].fillna(data[numeric_cols].mean())

        # 标准化前,确保没有NaN值
        data[numeric_cols] = np.nan_to_num(data[numeric_cols])

        # 标准化
        self.scaler = MinMaxScaler()
        scaled_data = self.scaler.fit_transform(data[numeric_cols])

        return scaled_data, numeric_cols

    def generate_sequences(self, data, lookback):
        """生成时间序列数据"""
        X, y = [], []
        for i in range(len(data) - lookback):
            X.append(data[i:i + lookback])
            y.append(data[i + lookback, :7])  # 只预测红球和蓝球
        return np.array(X), np.array(y)

    def enhanced_postprocessing(self, prediction):
        """改进的后处理方法,增强稳健性"""
        try:
            # 首先确保预测结果不含NaN值
            prediction = np.nan_to_num(prediction, nan=0.5)

            # 检查预测数据形状
            if prediction.shape[0] == 0 or prediction.shape[1] < 7:
                print(f"警告: 预测结果形状不正确: {prediction.shape}")
                # 创建一个随机预测作为备选
                random_preds = np.random.random((1, 7))
                red_pred = random_preds[0][:6]
                blue_pred = random_preds[0][6]
            else:
                red_pred = prediction[0][:6]
                blue_pred = prediction[0][6]

            # 处理红球
            red_balls = []
            for i in range(6):
                # 再次检查并处理NaN值或无效值
                if np.isnan(red_pred[i]) or red_pred[i] < 0 or red_pred[i] > 1:
                    red_pred[i] = np.random.random()  # 使用随机值代替无效值

                # 将0-1之间的值映射到1-33的整数
                ball = int(round(red_pred[i] * 32 + 1))
                ball = max(1, min(33, ball))  # 确保在合法范围内

                # 避免重复
                attempt = 0
                while ball in red_balls and attempt < 10:
                    # 避免死循环
                    if ball < 33:
                        ball += 1
                    else:
                        ball = max(1, ball - 1)
                    attempt += 1

                # 如果经过10次尝试后仍然有重复,生成一个不在当前列表中的随机号码
                if ball in red_balls:
                    available = [num for num in range(1, 34) if num not in red_balls]
                    if available:
                        ball = np.random.choice(available)

                red_balls.append(ball)

            # 确保红球排序
            red_balls = sorted(red_balls)

            # 处理蓝球
            # 再次检查并处理NaN值或无效值
            if np.isnan(blue_pred) or blue_pred < 0 or blue_pred > 1:
                blue_pred = np.random.random()  # 使用随机值代替无效值

            # 将0-1之间的值映射到1-16的整数
            blue_ball = int(round(blue_pred * 15 + 1))
            blue_ball = max(1, min(16, blue_ball))  # 确保在合法范围内

            return red_balls, blue_ball

        except Exception as e:
            print(f"后处理过程中出错: {str(e)}")
            # 生成随机预测作为备选
            red_balls = sorted(np.random.choice(range(1, 34), 6, replace=False))
            blue_ball = np.random.randint(1, 17)
            return red_balls, blue_ball

    def run(self):
        try:
            # 1. 数据加载
            self.progress.emit(5)
            data = pd.read_csv('data.csv')

            # 1.1 修复日期列 - 使用更强大的日期处理方法
            data = self.fix_date_column(data)

            # 确保数据为数值型,但排除日期列
            for col in data.columns:
                if col not in ['期号', '日期', 'date', 'clean_date']:
                    try:
                        data[col] = pd.to_numeric(data[col], errors='coerce')
                    except Exception as e:
                        print(f"无法将列 {col} 转换为数值型: {str(e)}")

            # 填充可能的NaN值
            numeric_cols = [col for col in data.columns if col not in ['期号', '日期', 'date', 'clean_date']]
            data[numeric_cols] = data[numeric_cols].fillna(data[numeric_cols].mean())

            # 2. 统计分析
            self.progress.emit(10)
            stats = self.calculate_enhanced_stats(data)
            self.stats_ready.emit(stats)
            self.red_stats = stats['red']
            self.blue_stats = stats['blue']

            # 3. 数据预处理
            self.progress.emit(20)
            scaled_data, feature_names = self.prepare_data(data)

            # 确保scaled_data中没有NaN值
            scaled_data = np.nan_to_num(scaled_data, nan=0.5)

            # 4. 划分训练集和测试集
            self.progress.emit(30)
            train_size = int(len(scaled_data) * self.train_ratio)
            train_data = scaled_data[:train_size]
            test_data = scaled_data[train_size:]

            # 5. 生成序列数据
            self.progress.emit(40)
            X_train, y_train = self.generate_sequences(train_data, self.lookback)
            X_test, y_test = self.generate_sequences(test_data, self.lookback)

            # 再次确保训练数据不含NaN值
            X_train = np.nan_to_num(X_train, nan=0.5)
            y_train = np.nan_to_num(y_train, nan=0.5)
            X_test = np.nan_to_num(X_test, nan=0.5)
            y_test = np.nan_to_num(y_test, nan=0.5)

            # 6. 创建并训练模型
            self.progress.emit(50)
            if self.strategy == 'lstm':
                self.model = self.create_lstm_model((self.lookback, X_train.shape[2]))
            elif self.strategy == 'gru':
                self.model = self.create_gru_model((self.lookback, X_train.shape[2]))
            else:
                self.model = self.create_hybrid_model((self.lookback, X_train.shape[2]))

            # 自定义回调函数用于更新进度
            class ProgressCallback(tf.keras.callbacks.Callback):
                def __init__(self, progress_signal):
                    super().__init__()
                    self.progress_signal = progress_signal
                    self.epoch_count = 0

                def on_epoch_end(self, epoch, logs=None):
                    self.epoch_count += 1
                    progress = 50 + (self.epoch_count / self.params['epochs']) * 40
                    self.progress_signal.emit(int(progress))

            # 训练模型
            history = self.model.fit(
                X_train, y_train,
                epochs=self.epochs,
                batch_size=self.batch_size,
                validation_data=(X_test, y_test),
                callbacks=[
                    EarlyStopping(monitor='val_loss', patience=20),
                    ModelCheckpoint('best_model.h5', save_best_only=True),
                    ProgressCallback(self.progress)
                ],
                verbose=0
            )

            # 8. 预测下一期
            self.progress.emit(95)
            last_data = scaled_data[-self.lookback:]
            last_data = last_data[None, ...]
            # 确保预测数据不含NaN值
            last_data = np.nan_to_num(last_data, nan=0.5)

            # 检查预测数据的形状和是否包含NaN值
            print(f"预测数据形状: {last_data.shape}")
            nan_count = np.isnan(last_data).sum()
            if nan_count > 0:
                print(f"警告: 预测数据中有{nan_count}个NaN值,已替换为0.5")

            prediction = self.model.predict(last_data)

            # 检查预测结果
            print(f"预测结果形状: {prediction.shape}")
            nan_count = np.isnan(prediction).sum()
            if nan_count > 0:
                print(f"警告: 预测结果中有{nan_count}个NaN值,已替换为0.5")

            # 确保预测结果不含NaN值
            prediction = np.nan_to_num(prediction, nan=0.5)

            # 9. 后处理
            red_balls, blue_ball = self.enhanced_postprocessing(prediction)

            # 11. 生成最终结果
            result = self.generate_result(red_balls, blue_ball, stats)

            self.progress.emit(100)
            self.model_trained.emit(self.model)
            self.finished.emit(result)

        except Exception as e:
            import traceback
            error_details = traceback.format_exc()
            self.finished.emit(f"预测失败:{str(e)}\n\n详细错误信息:\n{error_details}")

    def generate_result(self, red_balls, blue_ball, stats):
        """生成预测结果"""
        result = "=== 双色球优化预测结果 ===\n\n"
        result += f"预测号码:\n红球: {', '.join(map(str, red_balls))}\n蓝球: {blue_ball}\n\n"

        result += "=== 模型参数 ===\n"
        result += f"模型类型: {self.get_model_type()}\n"
        result += f"网络结构: {self.get_model_architecture()}\n"
        result += f"训练集比例: {int(self.train_ratio * 100)}%\n"
        result += f"训练轮次: {self.epochs}\n"
        result += f"批量大小: {self.batch_size}\n"
        result += f"回溯期数: {self.lookback}\n"

        return result

    def format_prediction_result(self, result):
        """将预测结果格式化为HTML"""
        # 解析预测结果
        lines = result.split('\n')

        # 提取预测号码
        red_balls = []
        blue_ball = None

        for line in lines:
            if line.startswith("红球:"):
                red_balls = [int(ball.strip()) for ball in line.replace("红球:", "").split(',')]
            elif line.startswith("蓝球:"):
                blue_ball = int(line.replace("蓝球:", "").strip())

        # 构建HTML
        html = """
        <!DOCTYPE html>
        <html>
        <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>双色球预测结果</title>
        <style>
            @import url('https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700&display=swap');

            body {
                font-family: 'Poppins', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
                margin: 0;
                padding: 20px;
                background-color: #f9fafb;
                color: #1e293b;
            }

            /* Custom Scrollbar Styling */
            ::-webkit-scrollbar {
                width: 10px;
                height: 10px;
            }

            ::-webkit-scrollbar-track {
                background: #f1f1f1;
                border-radius: 10px;
            }

            ::-webkit-scrollbar-thumb {
                background: linear-gradient(to bottom, #4a6cf7, #3a0ca3);
                border-radius: 10px;
                border: 2px solid #f1f1f1;
            }

            ::-webkit-scrollbar-thumb:hover {
                background: linear-gradient(to bottom, #3a0ca3, #4a6cf7);
            }

            /* For Firefox */
            html {
                scrollbar-width: thin;
                scrollbar-color: #4a6cf7 #f1f1f1;
            }

            .container {
                max-width: 100%;
                margin: 0 auto;
            }

            h1 {
                text-align: center;
                color: #1e293b;
                font-size: 28px;
                font-weight: 700;
                margin-bottom: 20px;
                padding-bottom: 10px;
                border-bottom: 3px solid #4361ee;
            }

            .prediction-card {
                background-color: #ffffff;
                border-radius: 12px;
                box-shadow: 0 6px 12px rgba(0, 0, 0, 0.08);
                padding: 25px;
                margin-bottom: 30px;
                text-align: center;
            }

            .section-title {
                font-weight: 700;
                color: #1e293b;
                font-size: 20px;
                margin-bottom: 20px;
                padding-bottom: 10px;
                border-bottom: 1px solid #e2e8f0;
            }

            .ball-container {
                display: flex;
                justify-content: center;
                align-items: center;
                gap: 12px;
                padding: 20px 0;
                flex-wrap: wrap;
            }

            .red-ball {
                width: 65px;
                height: 65px;
                display: inline-block;
                text-align: center;
                line-height: 65px;
                background: linear-gradient(135deg, #ff6b6b, #e63946);
                color: white;
                border-radius: 50%;
                font-weight: 700;
                font-size: 26px;
                box-shadow: 0 4px 8px rgba(230, 57, 70, 0.4);
                margin: 0 3px;
                position: relative;
                overflow: hidden;
            }

            .red-ball:before {
                content: '';
                position: absolute;
                top: -5px;
                left: -5px;
                width: 20px;
                height: 20px;
                background-color: rgba(255, 255, 255, 0.3);
                border-radius: 50%;
            }

            .blue-ball {
                width: 65px;
                height: 65px;
                display: inline-block;
                text-align: center;
                line-height: 65px;
                background: linear-gradient(135deg, #48cae4, #0077b6);
                color: white;
                border-radius: 50%;
                font-weight: 700;
                font-size: 26px;
                box-shadow: 0 4px 8px rgba(0, 119, 182, 0.4);
                margin: 0 3px;
                position: relative;
                overflow: hidden;
            }

            .blue-ball:before {
                content: '';
                position: absolute;
                top: -5px;
                left: -5px;
                width: 20px;
                height: 20px;
                background-color: rgba(255, 255, 255, 0.3);
                border-radius: 50%;
            }

            .separator {
                color: #94a3b8;
                font-weight: 300;
                font-size: 20px;
            }

            .param-section {
                background-color: #ffffff;
                border-radius: 12px;
                box-shadow: 0 6px 12px rgba(0, 0, 0, 0.08);
                padding: 25px;
                margin-bottom: 30px;
                position: relative;
                overflow: hidden;
            }

            .param-section::before {
                content: '';
                position: absolute;
                left: 0;
                top: 0;
                height: 100%;
                width: 4px;
                background-color: #4361ee;
            }

            .param {
                display: flex;
                justify-content: space-between;
                align-items: center;
                padding: 10px 15px;
                margin: 10px 0;
                background-color: #f8fafc;
                border-radius: 8px;
            }

            .param:hover {
                background-color: #f1f5f9;
            }

            .param-name {
                font-weight: 600;
                color: #1e293b;
            }

            .param-value {
                color: #64748b;
            }

            footer {
                text-align: center;
                margin-top: 30px;
                color: #94a3b8;
                font-size: 14px;
            }

            @media (max-width: 768px) {
                .red-ball, .blue-ball {
                    width: 55px;
                    height: 55px;
                    line-height: 55px;
                    font-size: 22px;
                }

                .param {
                    flex-direction: column;
                    align-items: flex-start;
                }

                .param-value {
                    margin-top: 5px;
                }
            }
        </style>
        </head>
        <body>
        <div class="container">
            <h1>双色球预测结果</h1>

            <div class="prediction-card">
                <div class="section-title">预测号码</div>
                <div class="ball-container">
        """

        # 添加红球
        if red_balls:
            for i, ball in enumerate(red_balls):
                html += f'<span class="red-ball">{ball}</span>'
                if i < len(red_balls) - 1:
                    html += f'<span class="separator"></span>'
                else:
                    html += f'<span class="separator">|</span>'

        # 添加蓝球
        if blue_ball:
            html += f'<span class="blue-ball">{blue_ball}</span>'

        html += """
                </div>
            </div>

            <div class="param-section">
                <div class="section-title">模型参数</div>
        """

        # 添加模型参数信息
        for line in lines:
            if "模型类型:" in line or "网络结构:" in line or "训练集比例:" in line or \
               "训练轮次:" in line or "批量大小:" in line or "回溯期数:" in line:
                parts = line.split(":", 1)
                if len(parts) == 2:
                    param_name = parts[0].strip()
                    param_value = parts[1].strip()
                    html += f'''
                    <div class="param">
                        <span class="param-name">{param_name}:</span>
                        <span class="param-value">{param_value}</span>
                    </div>
                    '''

        html += """
            </div>

            <footer>
                <p>© 2025 双色球预测系统 | 纯属娱乐,请理性购彩</p>
            </footer>
        </div>
        </body>
        </html>
        """

        return html

class StatsCanvas(FigureCanvas):
    def __init__(self, parent=None, width=5, height=4, dpi=100):
        self.fig = Figure(figsize=(width, height), dpi=dpi)
        self.fig.patch.set_facecolor('#F0F0F0')
        self.ax = self.fig.add_subplot(111)

        try:
            plt.rcParams['font.sans-serif'] = ['SimHei']
            plt.rcParams['axes.unicode_minus'] = False
        except:
            try:
                plt.rcParams['font.sans-serif'] = ['Microsoft YaHei']
            except:
                pass

        super().__init__(self.fig)
        self.setParent(parent)

    def plot_stats(self, stats, ball_type):
        """使用气泡图展示号码频率分布"""
        try:
            self.ax.clear()
            self.fig.patch.set_facecolor('#F8F9FA')
            self.ax.set_facecolor('#F8F9FA')

            if ball_type == 'red':
                title = '红球号码频率分布'
                data = stats['red']['all_time_sorted']
                cmap = plt.cm.Reds
                edge_color = '#C0392B'
                ball_range = range(1, 34)  # 红球1-33
            else:
                title = '蓝球号码频率分布'
                data = stats['blue']['all_time_sorted']
                cmap = plt.cm.Blues
                edge_color = '#2980B9'
                ball_range = range(1, 17)  # 蓝球1-16

            if not data:
                self.ax.set_title("无可用数据", fontsize=12, pad=15)
                self.fig.tight_layout()
                self.draw()
                return

            # 准备数据
            x = list(data.keys())
            y = [prob * 100 for prob in data.values()]

            # 计算平均值用于参考线
            avg = sum(y) / len(y)

            # 设置气泡大小比例
            sizes = [prob * 1000 + 100 for prob in data.values()]

            # 创建颜色映射
            norm = plt.Normalize(min(y), max(y))
            colors = cmap(norm(y))

            # 绘制气泡图
            scatter = self.ax.scatter(x, y, s=sizes, c=colors, 
                                     alpha=0.7, edgecolor=edge_color, linewidth=1)

            # 添加标签
            for i, (num, freq) in enumerate(zip(x, y)):
                # 只为TOP5的气泡添加标签
                if freq >= sorted(y, reverse=True)[min(4, len(y)-1)]:
                    self.ax.annotate(
                        f'{freq:.2f}%',
                        xy=(num, freq),
                        xytext=(0, 10),
                        textcoords='offset points',
                        ha='center',
                        fontsize=9,
                        fontweight='bold'
                    )

            # 添加水平参考线
            self.ax.axhline(y=avg, color='gray', linestyle='--', alpha=0.5, 
                           label=f'平均值: {avg:.2f}%')

            # 添加号码网格线
            self.ax.set_xticks(list(ball_range))
            self.ax.grid(axis='x', linestyle=':', alpha=0.3)

            # 美化图表
            self.ax.set_title(title, fontsize=14, fontweight='bold', pad=15)
            self.ax.set_xlabel('号码', fontsize=12)
            self.ax.set_ylabel('出现概率(%)', fontsize=12)
            self.ax.tick_params(axis='both', which='major', labelsize=10)
            self.ax.spines['top'].set_visible(False)
            self.ax.spines['right'].set_visible(False)

            # 设置x轴范围略宽于数据范围
            self.ax.set_xlim(min(ball_range) - 0.5, max(ball_range) + 0.5)

            # 设置y轴范围
            self.ax.set_ylim(0, max(y) * 1.15)

            # 添加颜色条
            cbar = self.fig.colorbar(scatter, ax=self.ax, pad=0.01, 
                                    format='%.1f%%', shrink=0.8)
            cbar.set_label('出现概率(%)', fontsize=10)

            # 添加图例
            self.ax.legend(loc='upper right')

            self.fig.tight_layout()
            self.draw()
        except Exception as e:
            print(f"绘制统计图表出错: {str(e)}")
            self.ax.clear()
            self.ax.text(0.5, 0.5, f'图表生成失败: {str(e)}', 
                         horizontalalignment='center',
                         verticalalignment='center',
                         transform=self.ax.transAxes,
                         fontsize=10)
            self.fig.tight_layout()
            self.draw()

class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("双色球预测系统 - 多维度神经网络模型 开发:Killerzeno 美化:nobiyou")
        self.setGeometry(100, 100, 1280, 900)
        self.setStyleSheet("""
            QMainWindow {
                background-color: #f5f7fa;
            }
            QTabWidget::pane {
                border: 1px solid #4a6cf7;
                border-top-left-radius: 0px;
                border-top-right-radius: 5px;
                border-bottom-left-radius: 5px;
                border-bottom-right-radius: 5px;
                background-color: #f5f7fa;
                margin: 0px;
                padding: 5px;
            }
            QTabBar::tab {
                background-color: #e1e5eb;
                color: #5e6470;
                min-width: 100px;
                min-height: 30px;
                padding: 5px 15px;
                border-top-left-radius: 5px;
                border-top-right-radius: 5px;
                margin-right: 2px;
                font-weight: bold;
            }
            QTabBar::tab:selected {
                background-color: #4a6cf7;
                color: white;
            }
            QGroupBox {
                background-color: white;
                border-radius: 10px;
                border: none;
                margin-top: 15px;
                font-weight: bold;
                padding: 15px;
                color: #2d3748;
            }
            QGroupBox::title {
                subcontrol-origin: margin;
                subcontrol-position: top left;
                padding: 0 10px;
                color: #4a6cf7;
                font-size: 14px;
            }
            QPushButton {
                background-color: #4a6cf7;
                color: white;
                border: none;
                border-radius: 5px;
                padding: 8px 15px;
                font-weight: bold;
                min-height: 30px;
            }
            QPushButton:hover {
                background-color: #3c5fe0;
            }
            QPushButton:pressed {
                background-color: #2d46bd;
            }
            QPushButton:disabled {
                background-color: #a0aec0;
            }
            QProgressBar {
                border: none;
                background-color: #e2e8f0;
                border-radius: 5px;
                text-align: center;
                color: white;
                font-weight: bold;
                min-height: 25px;
            }
            QProgressBar::chunk {
                background-color: #4a6cf7;
                border-radius: 5px;
            }
            QTextEdit, QLabel {
                background-color: white;
                border-radius: 5px;
                padding: 5px;
                border: 1px solid #e2e8f0;
            }
            QSpinBox, QDoubleSpinBox, QComboBox {
                border: 1px solid #e2e8f0;
                border-radius: 4px;
                padding: 5px;
                background-color: white;
                min-height: 25px;
            }
            QComboBox::drop-down {
                border: none;
                width: 20px;
            }
        """)

        # 设置图标
        icon_path = resource_path('logo.ico')
        if os.path.exists(icon_path):
            self.setWindowIcon(QIcon(icon_path))

        self.stats_data = None
        self.model = None

        # 设置Web通道
        self.channel = QWebChannel()
        self.handler = PageNavigator(self)
        self.channel.registerObject("pyObj", self.handler)

        self.initUI()

    def initUI(self):
        main_widget = QWidget()
        layout = QVBoxLayout()

        # 创建标签页
        self.tabs = QTabWidget()

        # 第一页 - 数据获取功能
        data_tab = QWidget()
        data_layout = QHBoxLayout()  # 使用水平布局,左右分栏

        # 左侧 - 操作区
        data_left_widget = QWidget()
        data_left_layout = QVBoxLayout()

        # 数据获取部分
        data_group = QGroupBox("数据获取操作")
        data_control_layout = QVBoxLayout()

        self.fetch_btn = QPushButton("获取最新双色球数据")
        self.fetch_btn.clicked.connect(self.fetch_data)
        data_control_layout.addWidget(self.fetch_btn)

        self.fetch_progress = QProgressBar()
        data_control_layout.addWidget(self.fetch_progress)

        self.data_status = QTextEdit()
        self.data_status.setReadOnly(True)
        self.data_status.setFixedHeight(200)
        data_control_layout.addWidget(self.data_status)

        data_group.setLayout(data_control_layout)
        data_left_layout.addWidget(data_group)
        data_left_layout.addStretch(1)  # 添加弹性空间
        data_left_widget.setLayout(data_left_layout)

        # 右侧 - 数据展示区
        data_right_widget = QWidget()
        data_right_layout = QVBoxLayout()

        data_display_group = QGroupBox("双色球数据展示")
        data_display_layout = QVBoxLayout()

        # 替换QTextEdit为QWebEngineView
        self.data_display = QWebEngineView()
        data_display_layout.addWidget(self.data_display)

        # 初始化分页变量
        self.current_page = 1
        self.items_per_page = 5
        self.total_data = None

        # 添加分页控制 - 隐藏显示,使用JS中的按钮
        pagination_widget = QWidget()
        pagination_layout = QHBoxLayout()
        pagination_layout.setContentsMargins(0, 0, 0, 0)

        # 网页风格分页按钮 - 隐藏显示
        self.page_prev_btn = QPushButton("上一页")
        self.page_prev_btn.setVisible(False)  # 隐藏按钮
        self.page_prev_btn.clicked.connect(lambda: self.change_data_page(-1))

        self.page_label = QLabel("第1页")
        self.page_label.setVisible(False)  # 隐藏标签

        self.page_next_btn = QPushButton("下一页")
        self.page_next_btn.setVisible(False)  # 隐藏按钮  
        self.page_next_btn.clicked.connect(lambda: self.change_data_page(1))

        pagination_layout.addWidget(self.page_prev_btn)
        pagination_layout.addWidget(self.page_label)
        pagination_layout.addWidget(self.page_next_btn)

        pagination_widget.setLayout(pagination_layout)
        pagination_widget.setVisible(False)  # 隐藏整个区域
        data_display_layout.addWidget(pagination_widget)

        data_display_group.setLayout(data_display_layout)
        data_right_layout.addWidget(data_display_group)
        data_right_widget.setLayout(data_right_layout)

        # 添加左右两部分到数据页面
        data_layout.addWidget(data_left_widget, 1)  # 左侧占1份宽度
        data_layout.addWidget(data_right_widget, 2)  # 右侧占2份宽度
        data_tab.setLayout(data_layout)

        # 第二页 - 预测功能
        predict_tab = QWidget()
        predict_layout = QHBoxLayout()  # 使用水平布局,左右分栏

        # 左侧 - 操作区
        predict_left_widget = QWidget()
        predict_left_layout = QVBoxLayout()

        # 预测参数设置
        param_group = QGroupBox("模型参数设置")
        param_layout = QFormLayout()

        # 训练集比例
        self.train_ratio = QDoubleSpinBox()
        self.train_ratio.setRange(50, 95)
        self.train_ratio.setValue(75)
        self.train_ratio.setSingleStep(5)
        self.train_ratio.setSuffix("%")
        param_layout.addRow("训练集比例 (推荐70-80%):", self.train_ratio)

        # 训练轮次
        self.epochs = QSpinBox()
        self.epochs.setRange(50, 1000)
        self.epochs.setValue(200)
        param_layout.addRow("训练轮次 (推荐100-300):", self.epochs)

        # 批量大小
        self.batch_size = QSpinBox()
        self.batch_size.setRange(16, 128)
        self.batch_size.setValue(32)
        param_layout.addRow("批量大小 (推荐32-64):", self.batch_size)

        # 回溯期数
        self.lookback = QSpinBox()
        self.lookback.setRange(5, 30)
        self.lookback.setValue(15)
        param_layout.addRow("回溯期数 (推荐10-20):", self.lookback)

        # 预测策略
        self.strategy = QComboBox()
        self.strategy.addItems(['LSTM', 'GRU', '混合模型'])
        self.strategy.setCurrentText('LSTM')
        param_layout.addRow("预测策略:", self.strategy)

        param_group.setLayout(param_layout)
        predict_left_layout.addWidget(param_group)

        # 预测按钮
        self.predict_btn = QPushButton("开始预测")
        self.predict_btn.clicked.connect(self.start_predict)
        predict_left_layout.addWidget(self.predict_btn)

        # 预测进度
        self.predict_progress = QProgressBar()
        predict_left_layout.addWidget(self.predict_progress)

        predict_left_layout.addStretch(1)  # 添加弹性空间
        predict_left_widget.setLayout(predict_left_layout)

        # 右侧 - 结果展示区
        predict_right_widget = QWidget()
        predict_right_layout = QVBoxLayout()

        result_group = QGroupBox("预测结果")
        result_layout = QVBoxLayout()
        self.result_display = QWebEngineView()
        result_layout.addWidget(self.result_display)
        result_group.setLayout(result_layout)

        predict_right_layout.addWidget(result_group)
        predict_right_widget.setLayout(predict_right_layout)

        # 添加左右两部分到预测页面
        predict_layout.addWidget(predict_left_widget, 1)  # 左侧占1份宽度
        predict_layout.addWidget(predict_right_widget, 2)  # 右侧占2份宽度
        predict_tab.setLayout(predict_layout)

        # 第三页 - 统计分析
        stats_tab = QWidget()
        stats_layout = QVBoxLayout()

        # 统计结果显示 - 使用HTML气泡图
        stats_group = QGroupBox("号码频率统计分析")
        stats_layout_inner = QVBoxLayout()
        self.stats_display = QWebEngineView()
        stats_layout_inner.addWidget(self.stats_display)
        stats_group.setLayout(stats_layout_inner)
        stats_layout.addWidget(stats_group)

        stats_tab.setLayout(stats_layout)

        # 添加标签页
        self.tabs.addTab(data_tab, "数据获取")
        self.tabs.addTab(predict_tab, "开始预测")
        self.tabs.addTab(stats_tab, "统计分析")

        # 添加到主布局
        layout.addWidget(self.tabs)
        main_widget.setLayout(layout)
        self.setCentralWidget(main_widget)

        # 初始化所有WebEngineView的WebChannel
        self.data_display.page().setWebChannel(self.channel)
        self.result_display.page().setWebChannel(self.channel)
        self.stats_display.page().setWebChannel(self.channel)

        # 添加状态栏
        self.statusBar().showMessage("就绪")

    def change_data_page(self, direction):
        """切换数据显示页"""
        if self.total_data is None or len(self.total_data) == 0:
            return

        new_page = self.current_page + direction
        max_page = (len(self.total_data) + self.items_per_page - 1) // self.items_per_page

        if 1 <= new_page <= max_page:
            self.current_page = new_page

            # 计算当前页的数据范围
            start_idx = (self.current_page - 1) * self.items_per_page
            end_idx = min(start_idx + self.items_per_page, len(self.total_data))

            # 显示当前页的数据
            self.display_styled_data(self.total_data.iloc[start_idx:end_idx])

    def fetch_data(self):
        if not self.check_internet_connection():
            QMessageBox.warning(self, "警告", "无法连接到互联网,请检查网络连接!")
            return

        self.fetch_btn.setEnabled(False)
        self.data_status.append("正在获取数据...")
        self.fetch_progress.setValue(0)

        self.fetcher = DataFetcher()
        self.fetcher.finished.connect(self.on_fetch_finished)
        self.fetcher.progress.connect(self.fetch_progress.setValue)
        self.fetcher.start()

    def check_internet_connection(self):
        """检查网络连接"""
        try:
            requests.get('http://www.baidu.com', timeout=5)
            return True
        except:
            return False

    def on_fetch_finished(self, message):
        self.fetch_btn.setEnabled(True)
        self.data_status.append(message)
        self.data_status.append("=" * 50)
        self.statusBar().showMessage("数据获取完成")

        # 在数据获取完成后显示数据
        try:
            if os.path.exists('data.csv'):
                df = pd.read_csv('data.csv')
                # 存储总数据
                self.total_data = df
                # 重置页码
                self.current_page = 1
                # 显示第一页
                self.display_styled_data(df.head(self.items_per_page))

                # 更新分页按钮状态
                self.page_prev_btn.setEnabled(False)  # 第一页,禁用上一页按钮
                if len(df) > self.items_per_page:
                    self.page_next_btn.setEnabled(True)
                else:
                    self.page_next_btn.setEnabled(False)
        except Exception as e:
            error_html = f"""
            <html><body>
            <h1 style="color: red; text-align: center;">数据加载错误</h1>
            <p style="text-align: center;">读取数据失败:{str(e)}</p>
            </body></html>
            """
            self.data_display.setHtml(error_html)

            # 禁用分页按钮
            self.page_prev_btn.setEnabled(False)
            self.page_next_btn.setEnabled(False)

    def display_styled_data(self, data):
        """显示带有样式的双色球数据"""
        # 获取当前页码和总页数
        max_page = (len(self.total_data) + self.items_per_page - 1) // self.items_per_page if self.total_data is not None else 1

        html = """
        <!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>双色球历史数据</title>
<script src="qrc:///qtwebchannel/qwebchannel.js"></script>
<script>
// 设置Web通道,连接Python对象
var pyObj;
window.onload = function() {
    new QWebChannel(qt.webChannelTransport, function(channel) {
        pyObj = channel.objects.pyObj;
    });
}

// 分页函数
function prevPage() {
    if (pyObj) {
        pyObj.prevPage();
    } else {
        console.error("Python对象未加载");
    }
}

function nextPage() {
    if (pyObj) {
        pyObj.nextPage();
    } else {
        console.error("Python对象未加载");
    }
}
</script>
<style>
    @import url('https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700&display=swap');

    :root {
        --primary-color: #4361ee;
        --red-ball-color: #e63946;
        --blue-ball-color: #0077b6;
        --background-color: #f9fafb;
        --card-background: #ffffff;
        --text-primary: #1e293b;
        --text-secondary: #64748b;
        --border-radius: 12px;
        --box-shadow: 0 10px 20px rgba(0, 0, 0, 0.08);
    }

    * {
        margin: 0;
        padding: 0;
        box-sizing: border-box;
    }

    /* Custom Scrollbar Styling */
    ::-webkit-scrollbar {
        width: 10px;
        height: 10px;
    }

    ::-webkit-scrollbar-track {
        background: #f1f1f1;
        border-radius: 10px;
    }

    ::-webkit-scrollbar-thumb {
        background: linear-gradient(to bottom, #4a6cf7, #3a0ca3);
        border-radius: 10px;
        border: 2px solid #f1f1f1;
    }

    ::-webkit-scrollbar-thumb:hover {
        background: linear-gradient(to bottom, #3a0ca3, #4a6cf7);
    }

    /* For Firefox */
    html {
        scrollbar-width: thin;
        scrollbar-color: #4a6cf7 #f1f1f1;
    }

    body {
        font-family: 'Poppins', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
        background-color: var(--background-color);
        color: var(--text-primary);
        padding: 30px 20px;
        line-height: 1.6;
    }

    .container {
        max-width: 1200px;
        margin: 0 auto;
    }

    header {
        text-align: center;
        margin-bottom: 40px;
    }

    h1 {
        color: var(--text-primary);
        font-size: 2.5rem;
        font-weight: 700;
        margin-bottom: 10px;
        position: relative;
        display: inline-block;
    }

    h1:after {
        content: '';
        position: absolute;
        bottom: -10px;
        left: 50%;
        transform: translateX(-50%);
        width: 100px;
        height: 4px;
        background: linear-gradient(to right, var(--red-ball-color), var(--blue-ball-color));
        border-radius: 4px;
    }

    .header-desc {
        color: var(--text-secondary);
        font-size: 1.1rem;
        max-width: 600px;
        margin: 20px auto 0;
    }

    .data-card {
        background-color: var(--card-background);
        border-radius: var(--border-radius);
        box-shadow: var(--box-shadow);
        overflow: hidden;
        margin-bottom: 30px;
    }

    table {
        width: 100%;
        border-collapse: collapse;
    }

    th, td {
        padding: 18px 15px;
        text-align: center;
    }

    th {
        background: linear-gradient(to right, var(--primary-color), #3a0ca3);
        color: white;
        font-weight: 600;
        font-size: 1rem;
        letter-spacing: 0.5px;
        text-transform: uppercase;
    }

    tr:nth-child(even) {
        background-color: rgba(243, 244, 246, 0.7);
    }

    tr:hover {
        background-color: rgba(224, 231, 255, 0.5);
        transition: all 0.3s ease;
    }

    .ball-container {
        display: flex;
        justify-content: center;
        align-items: center;
        flex-wrap: wrap;
        gap: 8px;
    }

    .ball {
        width: 45px;
        height: 45px;
        display: flex;
        align-items: center;
        justify-content: center;
        border-radius: 50%;
        font-weight: 700;
        font-size: 1.1rem;
        box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
        position: relative;
        overflow: hidden;
        transition: transform 0.2s, box-shadow 0.2s;
    }

    .ball:hover {
        transform: translateY(-3px) scale(1.05);
        box-shadow: 0 6px 12px rgba(0, 0, 0, 0.2);
    }

    .red-ball {
        background: linear-gradient(135deg, #ff6b6b, var(--red-ball-color));
        color: white;
    }

    .blue-ball {
        background: linear-gradient(135deg, #48cae4, var(--blue-ball-color));
        color: white;
    }

    .ball::before {
        content: '';
        position: absolute;
        top: -5px;
        left: -5px;
        width: 20px;
        height: 20px;
        background-color: rgba(255, 255, 255, 0.3);
        border-radius: 50%;
    }

    .separator {
        color: var(--text-secondary);
        font-weight: 300;
        margin: 0 2px;
    }

    .issue {
        font-weight: 600;
        font-size: 1rem;
        color: var(--text-primary);
    }

    .date {
        font-size: 0.95rem;
        color: var(--text-secondary);
    }

    .pagination {
        display: flex;
        justify-content: center;
        align-items: center;
        padding: 15px 0 25px 0;
    }

    .pagination-button {
        background-color: #f2f3f5;
        color: #4361ee;
        border: 1px solid #e2e8f0;
        border-radius: 3px;
        padding: 6px 15px;
        margin: 0 5px;
        font-size: 14px;
        cursor: pointer;
        transition: all 0.2s ease;
    }

    .pagination-button:hover {
        background-color: #e2e8f0;
    }

    .pagination-current {
        background-color: #4361ee;
        color: white;
        border-radius: 3px;
        padding: 6px 15px;
        margin: 0 5px;
        font-size: 14px;
    }

    footer {
        text-align: center;
        margin-top: 40px;
        color: var(--text-secondary);
        font-size: 0.9rem;
    }

    @media (max-width: 768px) {
        body {
            padding: 20px 10px;
        }

        h1 {
            font-size: 1.8rem;
        }

        .header-desc {
            font-size: 1rem;
        }

        th, td {
            padding: 12px 8px;
        }

        th {
            font-size: 0.9rem;
        }

        .ball {
            width: 40px;
            height: 40px;
            font-size: 1rem;
        }
    }

    @media (max-width: 576px) {
        .ball-container {
            gap: 5px;
        }

        .ball {
            width: 35px;
            height: 35px;
            font-size: 0.9rem;
        }

        .separator {
            display: inline-block;
            width: 5px;
        }

        th {
            font-size: 0.8rem;
        }
    }
</style>
</head>
<body>
<div class="container">
    <header>
        <h1>双色球历史数据</h1>
        <p class="header-desc">查看历史开奖结果,分析走势,把握规律</p>
    </header>

    <div class="data-card">
        <table>
            <thead>
                <tr>
                    <th width="20%">期号</th>
                    <th width="20%">日期</th>
                    <th width="60%">开奖号码</th>
                </tr>
            </thead>
            <tbody>
        """

        # 添加数据行
        for _, row in data.iterrows():
            html += f"""
            <tr>
                <td class="issue">第{row['期号']}期</td>
                <td class="date">{row['日期']}</td>
                <td>
                    <div class="ball-container">
            """

            # 添加红球
            for i in range(1, 7):
                html += f'<div class="ball red-ball">{int(row[f"red{i}"])}</div>'
                if i < 6:
                    html += f'<span class="separator"></span>'
                else:
                    html += f'<span class="separator">|</span>'

            # 添加蓝球
            html += f'<div class="ball blue-ball">{int(row["blue"])}</div>'

            html += """
                    </div>
                </td>
            </tr>
            """

        # 添加分页控件
        html += """
                </tbody>
            </table>

            <!-- 添加网页版分页 -->
            <div class="pagination">
        """

        # 动态生成上一页按钮
        if self.current_page > 1:
            html += f'<button id="prevPage" class="pagination-button">上一页</button>'
        else:
            html += f'<button disabled class="pagination-button" style="opacity:0.5;cursor:not-allowed;">上一页</button>'

        # 当前页码
        html += f'<span class="pagination-current">第{self.current_page}页</span>'

        # 动态生成下一页按钮
        if self.current_page < max_page:
            html += f'<button id="nextPage" class="pagination-button">下一页</button>'
        else:
            html += f'<button disabled class="pagination-button" style="opacity:0.5;cursor:not-allowed;">下一页</button>'

        html += """
            </div>
        </div>

        <footer>
            <p>© 2025 双色球数据统计 | 仅供参考,请理性购彩</p>
        </footer>
    </div>
    </body>
    </html>
        """

        # 设置HTML内容
        self.data_display.setHtml(html)

        # 连接Web通道
        self.data_display.page().setWebChannel(self.channel)

        # 移除旧的代码
        # script = """
        #     document.getElementById('prevPage')?.addEventListener('click', function() {
        #         pyObj.prev_page();
        #     });
        #     document.getElementById('nextPage')?.addEventListener('click', function() {
        #         pyObj.next_page();
        #     });
        # """
        # 
        # # 创建JavaScript对象,绑定Python方法
        # class JsObject(QObject):
        #     def __init__(self, parent=None):
        #         super().__init__(parent)
        #         self.main_window = parent
        #         
        #     @pyqtSlot()
        #     def prev_page(self):
        #         self.main_window.change_data_page(-1)
        #         
        #     @pyqtSlot()
        #     def next_page(self):
        #         self.main_window.change_data_page(1)
        # 
        # self.js_object = JsObject(self)
        # self.data_display.page().runJavaScript(script)

    def change_data_page(self, direction):
        """切换数据显示页"""
        if self.total_data is None or len(self.total_data) == 0:
            return

        new_page = self.current_page + direction
        max_page = (len(self.total_data) + self.items_per_page - 1) // self.items_per_page

        if 1 <= new_page <= max_page:
            self.current_page = new_page

            # 计算当前页的数据范围
            start_idx = (self.current_page - 1) * self.items_per_page
            end_idx = min(start_idx + self.items_per_page, len(self.total_data))

            # 显示当前页的数据
            self.display_styled_data(self.total_data.iloc[start_idx:end_idx])

    def start_predict(self):
        if not os.path.exists('data.csv'):
            QMessageBox.warning(self, "警告", "请先获取数据!")
            return

        self.predict_btn.setEnabled(False)

        # 创建一个加载动画HTML - 移除进度条的自动动画,改为通过JavaScript更新
        loading_html = """
        <!DOCTYPE html>
        <html>
        <head>
            <meta charset="UTF-8">
            <meta name="viewport" content="width=device-width, initial-scale=1.0">
            <title>加载中</title>
            <style>
                @import url('https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700&display=swap');

                /* Custom Scrollbar Styling */
                ::-webkit-scrollbar {
                    width: 10px;
                    height: 10px;
                }

                ::-webkit-scrollbar-track {
                    background: #f1f1f1;
                    border-radius: 10px;
                }

                ::-webkit-scrollbar-thumb {
                    background: linear-gradient(to bottom, #4a6cf7, #3a0ca3);
                    border-radius: 10px;
                    border: 2px solid #f1f1f1;
                }

                ::-webkit-scrollbar-thumb:hover {
                    background: linear-gradient(to bottom, #3a0ca3, #4a6cf7);
                }

                /* For Firefox */
                html {
                    scrollbar-width: thin;
                    scrollbar-color: #4a6cf7 #f1f1f1;
                }

                body {
                    font-family: 'Poppins', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
                    margin: 0;
                    padding: 0;
                    display: flex;
                    align-items: center;
                    justify-content: center;
                    flex-direction: column;
                    min-height: 100vh;
                    background-color: #f9fafb;
                    color: #1e293b;
                }

                .loader-container {
                    display: flex;
                    flex-direction: column;
                    align-items: center;
                    justify-content: center;
                    text-align: center;
                    padding: 30px;
                    background-color: white;
                    border-radius: 16px;
                    box-shadow: 0 10px 25px rgba(0, 0, 0, 0.08);
                    width: 90%;
                    max-width: 500px;
                    position: relative;
                    overflow: hidden;
                }

                .loader-container::before {
                    content: '';
                    position: absolute;
                    top: 0;
                    left: 0;
                    right: 0;
                    height: 4px;
                    background: linear-gradient(90deg, #4a6cf7, #3a0ca3, #4a6cf7);
                    background-size: 200% 100%;
                    animation: gradientMove 2s linear infinite;
                }

                @keyframes gradientMove {
                    0% {
                        background-position: 100% 0;
                    }
                    100% {
                        background-position: 0 0;
                    }
                }

                .loader {
                    position: relative;
                    width: 120px;
                    height: 120px;
                    margin-bottom: 20px;
                }

                .lottery-balls {
                    position: absolute;
                    top: 50%;
                    left: 50%;
                    transform: translate(-50%, -50%);
                    width: 100px;
                    height: 100px;
                }

                .ball {
                    position: absolute;
                    width: 30px;
                    height: 30px;
                    border-radius: 50%;
                    box-shadow: 0 3px 10px rgba(0, 0, 0, 0.2);
                    display: flex;
                    align-items: center;
                    justify-content: center;
                    color: white;
                    font-weight: bold;
                    font-size: 14px;
                    animation: bounce 1.5s ease-in-out infinite alternate;
                }

                @keyframes bounce {
                    0% {
                        transform: translateY(0);
                    }
                    100% {
                        transform: translateY(-20px);
                    }
                }

                .red-ball {
                    background: linear-gradient(135deg, #ff6b6b, #e63946);
                    border: 1.5px solid #C0392B;
                }

                .blue-ball {
                    background: linear-gradient(135deg, #48cae4, #0077b6);
                    border: 1.5px solid #2980B9;
                }

                .ball:nth-child(1) {
                    top: 10px;
                    left: 10px;
                    animation-delay: 0s;
                }

                .ball:nth-child(2) {
                    top: 10px;
                    right: 10px;
                    animation-delay: 0.2s;
                }

                .ball:nth-child(3) {
                    bottom: 10px;
                    left: 10px;
                    animation-delay: 0.4s;
                }

                .ball:nth-child(4) {
                    bottom: 10px;
                    right: 10px;
                    animation-delay: 0.6s;
                }

                .status-text {
                    font-size: 20px;
                    font-weight: 600;
                    color: #1e293b;
                    margin: 10px 0;
                }

                .info-text {
                    font-size: 14px;
                    color: #64748b;
                    line-height: 1.6;
                    max-width: 400px;
                }

                .phase {
                    margin-top: 20px;
                    font-size: 13px;
                    color: #4a6cf7;
                    font-weight: 500;
                    text-transform: uppercase;
                    letter-spacing: 1px;
                    opacity: 0;
                    animation: fadeInOut 6s infinite;
                }

                @keyframes fadeInOut {
                    0%, 100% { opacity: 0; }
                    16%, 84% { opacity: 1; }
                }

                #phase1 { animation-delay: 0s; }
                #phase2 { animation-delay: 2s; }
                #phase3 { animation-delay: 4s; }

                .progress-container {
                    width: 80%;
                    height: 6px;
                    background-color: #e2e8f0;
                    border-radius: 10px;
                    margin-top: 20px;
                    overflow: hidden;
                    position: relative;
                }

                .progress-bar {
                    position: absolute;
                    height: 100%;
                    background: linear-gradient(90deg, #4a6cf7, #3a0ca3);
                    border-radius: 10px;
                    width: 0%;
                    transition: width 0.3s ease;
                }

                .percentage {
                    position: absolute;
                    right: -30px;
                    top: -7px;
                    font-size: 12px;
                    font-weight: 600;
                    color: #4a6cf7;
                }
            </style>
        </head>
        <body>
            <div class="loader-container">
                <div class="loader">
                    <div class="lottery-balls">
                        <div class="ball red-ball">6</div>
                        <div class="ball red-ball">18</div>
                        <div class="ball red-ball">28</div>
                        <div class="ball blue-ball">12</div>
                    </div>
                </div>
                <div class="status-text">神经网络模型训练中</div>
                <div class="info-text">我们正在使用双色球历史数据训练深度学习模型,这可能需要几分钟时间。请耐心等待,您可以通过下方进度条查看训练进度。</div>

                <div class="phase" id="phase1">初始化模型架构</div>
                <div class="phase" id="phase2">优化神经网络参数</div>
                <div class="phase" id="phase3">生成预测结果</div>

                <div class="progress-container">
                    <div class="progress-bar" id="progressBar"></div>
                    <div class="percentage" id="progressText">0%</div>
                </div>
            </div>

            <script>
                // 提供一个函数供外部调用以更新进度条
                function updateProgress(progress) {
                    const progressBar = document.getElementById('progressBar');
                    const progressText = document.getElementById('progressText');

                    if (progressBar && progressText) {
                        progressBar.style.width = progress + '%';
                        progressText.textContent = progress + '%';

                        // 更新阶段提示
                        const phase1 = document.getElementById('phase1');
                        const phase2 = document.getElementById('phase2');
                        const phase3 = document.getElementById('phase3');

                        // 根据进度显示不同阶段
                        if (progress < 30) {
                            phase1.style.opacity = '1';
                            phase2.style.opacity = '0';
                            phase3.style.opacity = '0';
                            phase1.style.animation = 'none';
                        } else if (progress < 80) {
                            phase1.style.opacity = '0';
                            phase2.style.opacity = '1';
                            phase3.style.opacity = '0';
                            phase2.style.animation = 'none';
                        } else {
                            phase1.style.opacity = '0';
                            phase2.style.opacity = '0';
                            phase3.style.opacity = '1';
                            phase3.style.animation = 'none';
                        }
                    }
                }
            </script>
        </body>
        </html>
        """

        # 使用setHtml替代简单的文本提示
        self.result_display.setHtml(loading_html)
        self.predict_progress.setValue(0)

        # 连接进度条更新信号到我们的自定义函数
        self.predict_progress.valueChanged.connect(self.update_html_progress)

        train_ratio = self.train_ratio.value() / 100
        epochs = self.epochs.value()
        batch_size = self.batch_size.value()
        lookback = self.lookback.value()
        strategy = self.strategy.currentText().lower()

        self.predictor = EnhancedPredictor(train_ratio, epochs, batch_size, lookback, strategy)
        self.predictor.finished.connect(self.on_predict_finished)
        self.predictor.progress.connect(self.predict_progress.setValue)
        self.predictor.stats_ready.connect(self.update_stats)
        self.predictor.model_trained.connect(self.set_model)
        self.predictor.start()

    # 添加一个新函数,用于更新HTML进度条
    def update_html_progress(self, value):
        """更新HTML页面中的进度条"""
        # 使用JavaScript更新进度条
        script = f"updateProgress({value})"
        self.result_display.page().runJavaScript(script)

    def on_predict_finished(self, result):
        self.predict_btn.setEnabled(True)

        # 使用HTML格式美化预测结果
        html_result = self.predictor.format_prediction_result(result)
        self.result_display.setHtml(html_result)

        # 确保Web通道连接
        self.result_display.page().setWebChannel(self.channel)

        self.statusBar().showMessage("预测完成")

    def set_model(self, model):
        self.model = model

    def update_stats(self, stats):
        try:
            self.stats_data = stats

            # 使用bubbles模块生成HTML气泡图
            html = generate_bubble_chart_html(stats)

            # 设置HTML内容
            self.stats_display.setHtml(html)

            # 确保Web通道连接
            self.stats_display.page().setWebChannel(self.channel)
        except Exception as e:
            print(f"更新统计信息出错: {str(e)}")
            import traceback
            traceback_details = traceback.format_exc()
            print(f"详细错误: {traceback_details}")

            # 处理异常情况
            error_html = f"""
            <html><body>
            <h1 style="color: red; text-align: center;">统计数据处理错误</h1>
            <p style="text-align: center;">请重新获取数据。错误信息: {str(e)}</p>
            </body></html>
            """
            self.stats_display.setHtml(error_html)
            self.statusBar().showMessage("统计数据更新失败")

# 页面导航器,用于JavaScript与PyQt交互
class PageNavigator(QObject):
    def __init__(self, main_window):
        super().__init__()
        self.main_window = main_window

    @pyqtSlot()
    def prevPage(self):
        self.main_window.change_data_page(-1)

    @pyqtSlot()
    def nextPage(self):
        self.main_window.change_data_page(1)

if __name__ == "__main__":
    app = QApplication(sys.argv)
    icon_path = resource_path('logo.ico')
    if os.path.exists(icon_path):
        app.setWindowIcon(QIcon(icon_path))
    window = MainWindow()
    window.show()
    sys.exit(app.exec_())

Great works are not done by strength, but by persistence! 历尽艰辛的飞升者,成了围剿孙悟空的十万天兵之一。
相信我 学习就是不断的重复 不需要什么技巧
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则 需要先绑定手机号


免责声明:
本站所发布的第三方软件及资源(包括但不仅限于文字/图片/音频/视频等仅限用于学习和研究目的;不得将上述内容用于商业或者非法用途,否则,一切后果请用户自负。本站信息来自网络,版权争议与本站无关。您必须在下载后的24个小时之内,从您的电脑中彻底删除上述内容。如果您喜欢某程序或某个资源,请支持正版软件及版权方利益,注册或购买,得到更好的正版服务。如有侵权请邮件与我们联系处理。

Mail To: admin@cdsy.xyz

QQ|Archiver|手机版|小黑屋|城东书院 ( 湘ICP备19021508号-1|湘公网安备 43102202000103号 )

GMT+8, 2025-5-19 23:59 , Processed in 0.074033 second(s), 30 queries .

Powered by Discuz! CDSY.XYZ

Copyright © 2019-2023, Tencent Cloud.

快速回复 返回顶部 返回列表