Gray Code & One_hot encoding

本文深入探讨了灰码(GrayCode)与独热编码(One-hotEncoding)的概念、性质及其应用。通过实例展示了如何利用灰码的特性进行坐标变换,并解释了独热编码在状态机和电路设计中的优势。同时,提供了两种编码方式的对比分析,旨在帮助读者理解这些技术在实际场景中的应用价值。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1.  Gray Code (格雷码)

 

說明

 

Gray Code是一個數列集合,每個數使用二進位來表示,假設使用n位元來表示每個數好了,任兩個數之間只有一個位元值不同,例如以下為3位元的Gray Code:

 

000 001 011 010 110 111 101 100

 


由定義可以知道,Gray Code的順序並不是唯一的,例如將上面的數列反過來寫,也是一組Gray Code:

 

100 101 111 110 010 011 001 000

 


Gray Code是由貝爾實驗室的Frank Gray在1940年代提出的,用來在使用PCM(Pusle Code Modulation)方法傳送訊號時避免出錯,並於1953年三月十七日取得美國專利。

 

解法

 

由於Gray Code相鄰兩數之間只改變一個位元,所以可觀 察Gray Code從1變0或從0變1時的位置,假設有4位元的Gray Code如下:

 

0000 0001 0011 0010 0110 0111 0101 0100
1100 1101 1111 1110 1010 1011 1001 1000

 


觀察奇數項的變化時,我們發現無論它是第幾個Gray Code,永遠只改變最右邊的位元,如果是1就改為0,如果是0就改為1。

觀察偶數項的變化時,我們發現所改變的位元,是由右邊算來第一個1的左邊位元。

以上兩個變化規則是固定的,無論位元數為何;所以只要判斷位元的位置是奇數還是偶數,就可以決定要改變哪一個位元的值,為了程式撰寫方便,將陣列索引 0當作最右邊的值,而在列印結果時,是由索引數字大的開始反向列印。

將2位元的Gray Code當作平面座標來看,可以構成一個四邊形,您可以發現從任一頂點出發,繞四邊形周長繞一圈,所經過的頂點座標就是一組Gray Code,所以您可以得到四組Gray Code。

同樣的將3位元的Gray Code當作平面座標來看的話,可以構成一個正立方體,如果您可以從任一頂點出發,將所有的邊長走過,並不重複經過頂點的話,所經過的頂點座標順序之組合也就是一組Gray Code。

 

 

  

 

 

2.  One-hot Encoding (独热编码)

 

独热码是指对任意给定的状态,状态向量中只有1位为1,其余各位为0。N状态的状态机需要N个触发器,这种状态机的速度与状态的数量无关,只取决于某特定状态的转移数量,速度很快。当状态机的状态增加时,如果使用二编码,那么速度会明显下降,但如果采用独热码,虽然多用了触发器,但由于状态译码简单,节省和简化了组合逻辑电路。

 

In One-Hot coding, the encoding should be:

 

 

 

 State:        A    |     B    |      C    |     D

 

assignment:    0001  |  0010  |   0100  |   1000

increase the number of flip-flops you use, but has the advantage that needs less logic for the excitation equations.

 

举另外一个例子,比如:

十进制:       0       |        1       |         2      |         3        |         4        |       5        |       6       |        7       |       8         |       9 
独热码:000000000 | 000000001 | 000000010  |  000000100  |  000001000  | 000010000 | 000100000 | 001000000 | 010000000  | 100000000 

 

http://www.vlsibank.com/sessionspage.asp?titl_id=4867

 

Design artical:

 http://www.trilobyte.com/pdf/golson_pldcon93.pdf

 

对于寄存器数量多,而门逻辑相对缺乏的FPGA器件,采用独热码可以有效提高电路的速度和可靠性,也有利于提高器件资源的利用率。 

转载于:https://www.cnblogs.com/Jerome_Lee/archive/2010/03/23/1692536.html

修改问题:在子类界面(1.输入分析)里,用户点击刷新后,仍然没有把数据完整呈现出来,有空格。而用户点击按钮“删除”时,输入框里的数据又不能删除。。2.检查,保存和读取文件的路径[c:\用户 \administrator\桌面\saved_numbers.json]原代码import sys import threading import time import logging import json import random import os from typing import List, Dict, Optional, Callable, Any from tkinter import (Tk, Frame, Label, Button, Entry, StringVar, PanedWindow, HORIZONTAL, VERTICAL, RAISED, Canvas, Scrollbar, messagebox, Text, LabelFrame) # 添加LabelFrame from tkinter.constants import VERTICAL, RAISED from dataclasses import dataclass from enum import Enum, auto from queue import Queue 配置日志 logging.basicConfig( level=logging.INFO, format=‘%(asctime)s - %(name)s - %(levelname)s - %(message)s’, datefmt=‘%Y-%m-%d %H:%M:%S’) logger = logging.getLogger(‘DLT_Analysis’) ==================== 全局配置 ==================== class GlobalConfig: VERSION = “大乐透智能分析平台 v5.2” POOL_SAVE_PATH = os.path.join(os.path.expanduser(“~”), “Desktop”, “号码池.json”) MODULE1_SAVE_PATH = os.path.join(os.path.expanduser(“~”), “Desktop”, “saved_numbers.json”) MODULE1_ID = “module1.module1_id” # 新增模块ID MODULE2_ID = “module2.module2_id” # 新增 MODULE3_ID = “module3.module3_id” # 新增 MODULE4_ID = “module4.module4_id” # 新增 MODULE5_ID = “module5.module5_id” # 新增 MODULES = [‘input_analysis’, ‘combination_analysis’, ‘follow_analysis’, ‘trend_analysis’, ‘number_generation’] # 号码池UI配置 UI_CONFIG = [ (“前区:”, “front_area”, 1), (“后区:”, “back_area”, 2), (“前数字频:”, “front_freq”, 3), (“前数字缺:”, “front_missing”, 4), (“后数字频:”, “back_freq”, 5), (“后数字缺:”, “back_missing”, 6), (“前频繁推:”, “front_freq_50”, 7), (“后低频推:”, “back_infreq_50”, 8), (“生组合数:”, “comb_count”, 9), (“未组合码:”, “uncombined”, 10), (“前推荐多:”, “front_rec_more”, 11), (“前推荐少:”, “front_rec_less”, 12), (“后推荐多:”, “back_rec_more”, 13), (“后推荐少:”, “back_rec_less”, 14), (“和值:”, “sum_rec”, 15), (“质合比:”, “prime_rec”, 16), (“奇偶比:”, “odd_even_rec”, 17), (“断区推荐:”, “zone_rec”, 18), (“连号推荐:”, “consec_rec”, 19), (“冷热推荐:”, “hot_cold_rec”, 20), (“后区热号:”, “hot_rec”, 21), (“后区冷号:”, “cold_rec”, 22), (“趋势号:”, “trend_rec”, 23) ] 全局应用上下文 class AppContext: def init(self): self.dialog_manager = None self.main_ui = None self.loading_queue = Queue() self.modules_loaded = {module: False for module in GlobalConfig.MODULES} self.modules_completed = app = AppContext() ==================== 事件类型 ==================== class EventType(Enum): START_COMMAND = auto() DATA_SUBMIT = auto() ACK = auto() MODULE_READY = auto() MODULE_COMPLETE = auto() UI_UPDATE = auto() DIALOG_OPEN = auto() DIALOG_CLOSE = auto() DATA_FREEZE = auto() DATA_ORGANIZE = auto() # 添加缺失的枚举值 MODULE_RUN = auto() LOADING_PROGRESS = auto() LOADING_COMPLETE = auto() EXCLUDE_NUMBERS = auto() POOL_UPDATE = auto() ==================== 事件系统 ==================== @dataclass class Event: event_id: int type: EventType source: str target: str timestamp: float = time.time() data: Optional[Dict[str, Any]] = None class EventEmitter: def init(self): self._lock: threading.Lock = threading.Lock() self._subscribers: Dict[EventType, List[Callable[[Event], None]]] = {} def subscribe(self, event_type: EventType, callback: Callable[[Event], None]): with self._lock: if event_type not in self._subscribers: self._subscribers[event_type] = [] self._subscribers[event_type].append(callback) def publish(self, event: Event): with self._lock: subscribers = self._subscribers.get(event.type, []) for callback in subscribers: try: callback(event) except Exception as e: logging.error(f"事件处理失败: {str(e)}", exc_info=True) event_center = EventEmitter() ==================== 号码池实现 ==================== class NumberPool: def init(self): self.data_store: Dict[str, Any] = { ‘front_hot’: [], ‘back_hot’: [], ‘front_freq’: {}, ‘back_freq’: {}, ‘front_missing’: [], ‘back_missing’: [], ‘recommendations’: {}, ‘generated_numbers’: [], ‘frozen’: False, ‘front_numbers’: [], ‘back_numbers’: [], ‘special_data’: {}, ‘organized_data’: None } self.lock = threading.Lock() self.current_module: Optional[str] = None self.module_status = {module: False for module in GlobalConfig.MODULES} self.running = True self._setup_subscriptions() self._load_data() def update(self, label: str, value: Any): """更新号码池数据""" with self.lock: # 根据标签名更新对应数据 if label == "前区:": self.data_store['front_numbers'] = value elif label == "后区:": self.data_store['back_numbers'] = value elif label == "前数字频:": self.data_store['front_freq'] = value elif label == "前数字缺:": self.data_store['front_missing'] = value elif label == "后数字频:": self.data_store['back_freq'] = value elif label == "后数字缺:": self.data_store['back_missing'] = value elif label == "前频繁推:": self.data_store['front_freq_50'] = value elif label == "后低频推:": self.data_store['back_infreq_50'] = value elif label == "生组合数:": self.data_store['comb_count'] = value elif label == "未组合码:": self.data_store['uncombined'] = value elif label == "前推荐多:": self.data_store['front_rec_more'] = value elif label == "前推荐少:": self.data_store['front_rec_less'] = value elif label == "后推荐多:": self.data_store['back_rec_more'] = value elif label == "后推荐少:": self.data_store['back_rec_less'] = value elif label == "和值:": self.data_store['sum_rec'] = value elif label == "质合比:": self.data_store['prime_rec'] = value elif label == "奇偶比:": self.data_store['odd_even_rec'] = value elif label == "断区推荐:": self.data_store['zone_rec'] = value elif label == "连号推荐:": self.data_store['consec_rec'] = value elif label == "冷热推荐:": self.data_store['hot_cold_rec'] = value elif label == "后区热号:": self.data_store['hot_rec'] = value elif label == "后区冷号:": self.data_store['cold_rec'] = value elif label == "趋势号:": self.data_store['trend_rec'] = value self._save_data() def _setup_subscriptions(self): event_center.subscribe(EventType.DATA_SUBMIT, self._handle_data_submit) event_center.subscribe(EventType.MODULE_READY, self._handle_module_ready) event_center.subscribe(EventType.MODULE_COMPLETE, self._handle_module_complete) event_center.subscribe(EventType.DATA_FREEZE, self._handle_freeze) event_center.subscribe(EventType.DATA_ORGANIZE, self._handle_organize) event_center.subscribe(EventType.MODULE_RUN, self._handle_module_run) event_center.subscribe(EventType.EXCLUDE_NUMBERS, self._handle_exclude_numbers) def _handle_exclude_numbers(self, event: Event): """处理排除号码事件""" if event.data: exclude_front = event.data.get('exclude_front', '').split() exclude_back = event.data.get('exclude_back', '').split() # 更新排除号码 self.data_store['excluded_front'] = [int(x) for x in exclude_front if x.isdigit()] self.data_store['excluded_back'] = [int(x) for x in exclude_back if x.isdigit()] # 日志记录 logging.info(f"更新排除号码: 前区 {exclude_front}, 后区 {exclude_back}") def _handle_module_ready(self, event: Event): if event.target == 'pool': logging.info(f"模块 {event.source} 已就绪") app.modules_loaded[event.source] = True def _handle_module_complete(self, event: Event): if event.target == 'pool': logging.info(f"模块 {event.source} 已完成运行") self.module_status[event.source] = False self.current_module = None app.modules_completed[event.source] = True def _handle_freeze(self, event: Event): if event.target == 'pool': with self.lock: self.data_store['frozen'] = True logging.info("号码池数据已冻结") def _handle_organize(self, event: Event): if event.target == 'pool': with self.lock: # 检查所有模块是否已完成 all_completed = all(app.modules_completed.values()) if not all_completed: logging.warning("无法整理数据: 还有模块未完成分析") return # 整理数据逻辑 self._organize_data() logging.info("号码池数据已整理") # 更新UI update_event = Event( event_id=int(time.time()), type=EventType.UI_UPDATE, source='pool', target='main_ui', data={'update_type': 'organized_data', 'data': self.data_store['organized_data']} ) event_center.publish(update_event) def _organize_data(self): """整理号码池数据到核心展示区""" organized = { 'front_numbers': self._organize_front_numbers(), 'back_numbers': self._organize_back_numbers(), 'front_hot': self._organize_front_hot(), 'front_cold': self._organize_front_cold(), 'back_hot': self._organize_back_hot(), 'back_cold': self._organize_back_cold() } self.data_store['organized_data'] = organized def _organize_front_numbers(self): """整理前区号码""" front_numbers = self.data_store.get('front_numbers', []) uncombined = self.data_store.get('recommendations', {}).get('uncombined', []) return sorted(list(set(front_numbers + uncombined))) def _organize_back_numbers(self): """整理后区号码""" return self.data_store.get('back_numbers', []) def _organize_front_hot(self): """整理前区热号""" front_freq = list(self.data_store.get('front_freq', {}).keys()) front_freq_rec = self.data_store.get('recommendations', {}).get('front极_freq_50', []) front_rec_more = self.data_store.get('recommendations', {}).get('front_rec_more', []) hot_cold_rec = self.data_store.get('recommendations', {}).get('hot_cold_rec', {}).get('hot', []) hot_numbers = list(set( [int(x) for x in front_freq] + front_freq_rec + front_rec_more + hot_cold_rec )) return sorted(hot_numbers) def _organize_front_cold(self): """整理前区冷号""" front_missing = self.data_store.get('front_missing', []) front_rec_less = self.data_store.get('recommendations', {}).get('front_rec_less', []) hot_cold_rec = self.data_store.get('recommendations', {}).get('hot_cold_rec', {}).get('cold', []) cold_numbers = list(set( front_missing + front_rec_less + hot_cold_rec )) return sorted(cold_numbers) def _organize_back_hot(self): """整理后区热号""" back_freq = list(self.data_store.get('back_freq', {}).keys()) back_rec_more = self.data_store.get('recommendations', {}).get('back_rec_more', []) hot_rec = self.data_store.get('recommendations', {}).get('hot_rec', []) hot_numbers = list(set( [int(x) for x in back_freq] + back_rec_more + hot_rec )) return sorted(hot_numbers) def _organize_back_cold(self): """整理后区冷号""" back_missing = self.data_store.get('back_missing', []) back_infreq_rec = self.data_store.get('recommendations', {}).get('back_infreq_50', []) back_rec_less = self.data_store.get('recommendations', {}).get('back_rec_less', []) cold_rec = self.data_store.get('recommendations', {}).get('cold_rec', []) cold_numbers = list(set( back_missing + back_infreq_rec + back_rec_less + cold_rec )) return sorted(cold_numbers) def _handle_module_run(self, event: Event): if event.target == 'pool' and event.source in GlobalConfig.MODULES: self.current_module = event.source self.module_status[event.source] = True logging.info(f"模块 {event.source} 开始运行") run_event = Event( event_id=int(time.time()), type=EventType.MODULE_RUN, source='pool', target=event.source ) event_center.publish(run_event) def _handle_data_submit(self, event: Event): if event.target == 'pool' and not self.data_store['frozen']: with self.lock: logger.info(f"收到来自 {event.source} 的数据提交") # 新增日志 if event.data: self.data_store.update(event.data) logger.debug(f"更新后的数据: {self.data_store}") # 新增日志 self.data_store.update(event.data) update_event = Event( event_id=int(time.time()), type=EventType.UI_UPDATE, source='pool', target='main_ui', data={'update_type': 'pool_update', 'data': self.data_store} ) event_center.publish(update_event) self._save_data() def _save_data(self): try: with open(GlobalConfig.POOL_SAVE_PATH, 'w', encoding='utf-8') as f: json.dump(self.data_store, f, ensure_ascii=False, indent=2) except IOError as e: logging.error(f"[号码池] 保存数据失败: {str(e)}") def _load_data(self): if os.path.exists(GlobalConfig.POOL_SAVE_PATH): try: with open(GlobalConfig.POOL_SAVE_PATH, 'r', encoding='utf-8') as f: data = json.load(f) with self.lock: self.data_store.update(data) logging.info("[号码池] 成功加载之前保存的数据") except (IOError, json.JSONDecodeError) as e: logging.error(f"[号码池] 加载数据失败: {str(e)}") ==================== 基础模块类 ==================== class BaseModule: def init(self, module_name: str): self.module_name = module_name self._setup_subscriptions() self._initialize() self.dynamic_data = {} self._dialog_opened = False # 确保所有属性都被初始化 def _setup_subscriptions(self): def filtered_handler(event: Event): if event.target == self.module_name: self._handle_start_command(event) event_center.subscribe(EventType.START_COMMAND, filtered_handler) event_center.subscribe(EventType.MODULE_RUN, self._handle_module_run) def clear_dynamic_data(self): """清除动态区数据""" for key in self.dynamic_data: if isinstance(self.dynamic_data[key], list): self.dynamic_data[key] = [] elif isinstance(self.dynamic_data[key], dict): self.dynamic_data[key] = {} else: self.dynamic_data[key] = "" def save_dynamic_data(self, file_path): """保存动态区数据到指定位置""" with open(file_path, 'w') as f: json.dump(self.dynamic_data, f) def refresh_to_pool(self, number_pool): """将动态区数据传递到号码池对应标签""" for key, value in self.dynamic_data.items(): # 确保标签名与号码池标签名一致 pool_label = f"{key}:" if not key.endswith(":") else key number_pool.update(pool_label, value) def _initialize(self): ready_event = Event( event_id=int(time.time()), type=EventType.MODULE_READY, source=self.module_name, target='pool' ) event_center.publish(ready_event) def _handle_start_command(self, event: Event): if not self._dialog_opened: logger.info(f"正在打开模块 {self.module_name} 的界面") self._open_dialog() self._dialog_opened = True else: logging.warning(f"{self.module_name} 已打开,避免重复触发") def _open_dialog(self): # 使用主循环调度UI操作,确保在主线程执行 app.main_ui.root.after(0, lambda: event_center.publish(Event( event_id=int(time.time()), type=EventType.DIALOG_OPEN, source=self.module_name, target='dialog_manager', data={'dialog_type': self.module_name} ))) def _submit_data(self, data: Dict[str, Any]): submit_event = Event( event_id=int(time.time()), type=EventType.DATA_SUBMIT, source=self.module_name, target='pool', data=data ) event_center.publish(submit_event) def _handle_module_run(self, event: Event): """处理模块运行事件""" raise NotImplementedError(f"{self.__class__.__name__} 必须实现 _handle_module_run 方法") def run(self): raise NotImplementedError("子类必须实现run方法") ==================== 输入分析模块 ==================== class InputAnalysisModule(BaseModule): def init(self, module_name: str): super().init(module_name) # 统一标签名格式(添加冒号) self.dynamic_data = { “排除号码”: { “前区”: [], “后区”: [] }, “推荐号码”: { “前区”: [], “后区”: [] } } def _handle_module_run(self, event: Event): """处理输入分析模块的运行事件""" logger.info(f"开始运行输入分析模块: {event}") self.run() def run(self): dialog = app.dialog_manager.active_dialogs.get(self.module_name) if not dialog: return # 获取排除号码 exclude_front = app.main_ui.exclude_front_var.get() exclude_back = app.main_ui.exclude_back_var.get() # 更新动态区显示 self._update_dynamic_text(f"排除号码 - 前区: {exclude_front}, 后区: {exclude_back}\n") # 模拟分析过程 time.sleep(1) # 生成分析结果(考虑排除号码) all_front = [x for x in range(1, 36) if str(x) not in exclude_front.split()] all_back = [x for x in range(1, 13) if str(x) not in exclude_back.split()] front_nums = sorted(random.sample(all_front, 5)) back_nums = sorted(random.sample(all_back, 2)) # 更新界面显示 result_text = f"分析结果:\n前区: {front_nums}\n后区: {back_nums}\n" self._update_dynamic_text(result_text) # 提交数据到号码池 self._submit_data({ 'front_numbers': front_nums, 'back_numbers': back_nums, 'excluded_numbers': { 'front': exclude_front.split(), 'back': exclude_back.split() } }) # 标记模块完成 complete_event = Event( event_id=int(time.time()), type=EventType.MODULE_COMPLETE, source=self.module_name, target='pool' ) event_center.publish(complete_event) def _update_dynamic_text(self, text: str): dialog = app.dialog_manager.active_dialogs.get(self.module_name) if dialog and hasattr(dialog, 'dynamic_text'): dialog.dynamic_text.insert('end', text) ==================== 组合分析模块 ==================== class CombinationAnalysisModule(BaseModule): def init(self, module_name: str): super().init(module_name) # 统一标签名格式(添加冒号) self.dynamic_data = { “前数字频”: {}, “前数字缺”: [], “后数字频”: {}, “后数字缺”: [], “前频繁推”: [], “后低频推”: [], “生组合数”: 0, “未组合码”: [] } def _handle_module_run(self, event: Event): """处理组合分析模块的运行事件""" logger.info(f"开始运行组合分析模块: {event}") self.run() def run(self): dialog = app.dialog_manager.active_dialogs.get(self.module_name) if not dialog: return # 更新动态活动区显示 self._update_dynamic_text("开始组合分析...\n") # 模拟分析过程 time.sleep(1) # 生成分析结果 (模拟) front_hot = sorted(random.sample(range(1, 36), 5)) back_hot = sorted(random.sample(range(1, 13), 2)) front_freq = {str(i): random.randint(1, 100) for idx in range(1, 36)} back_freq = {str(i): random.randint(1, 100) for idx in range(1, 13)} front_missing = sorted(random.sample(range(1, 36), 5)) back_missing = sorted(random.sample(range(1, 13), 2)) front_freq_rec = sorted(random.sample(range(1, 36), 5)) back_infreq_rec = sorted(random.sample(range(1, 13), 2)) # 定义back_infreq_rec uncombined = [] # 定义uncombined变量 # 更新界面显示 result_text = f""" 组合分析完成: 前数字频: {front_freq} 前数字缺: {front_missing} 后数字频: {back_freq} 后数字缺: {back_missing} 前频繁推: {front_freq_rec} 后低频推: {back_infreq_rec} 生组合数: 0 未组合码: {uncombined} """ self._update_dynamic_text(result_text) # 提交数据到号码池 self._submit_data({ 'front_hot': front_hot, 'back_hot': back_hot, 'front_freq': front_freq, 'back_freq': back_freq, 'front_missing': front_missing, 'back_missing': back_missing, 'recommendations': { 'front_freq_50': front_freq_rec, 'back_infreq_50': back_infreq_rec, 'uncombined': uncombined } }) # 标记模块完成 complete_event = Event( event_id=int(time.time()), type=EventType.MODULE_COMPLETE, source=self.module_name, target='pool' ) event_center.publish(complete_event) def _update_dynamic_text(self, text: str): dialog = app.dialog_manager.active_dialogs.get(self.module_name) if dialog and hasattr(dialog, 'dynamic_text'): dialog.dynamic_text.insert('end', text) ==================== 跟随分析模块 ==================== class FollowAnalysisModule(BaseModule): def init(self, module_name: str): super().init(module_name) # 统一标签名格式(添加冒号) self.dynamic_data = { “前推荐多”: [], “前推荐少”: [], “后推荐多”: [], “后推荐极少”: [] } def _handle_module_run(self, event: Event): """处理跟随分析模块的运行事件""" logger.info(f"开始运行跟随分析模块: {event}") self.run() def run(self): dialog = app.dialog_manager.active_dialogs.get(self.module_name) if not dialog: return # 更新动态活动区显示 self._update_dynamic_text("开始跟随分析...\n") # 模拟分析过程 time.sleep(1) # 生成分析结果 (模拟) front_rec_more = sorted(random.sample(range(1, 36), 5)) front_rec_less = sorted(random.sample(range(1, 36), 5)) back_rec_more = sorted(random.sample(range(1, 13), 2)) back_rec_less = sorted(random.sample(range(1, 13), 2)) # 更新界面显示 result_text = f""" 跟随分析完成: 前推荐多: {front_rec_more} 前推荐少: {front_rec_less} 后推荐多: {back_rec_more} 后推荐少: {back_rec_less} """ self._update_dynamic_text(result_text) # 提交数据到号码池 self._submit_data({ 'recommendations': { 'front_rec_more': front_rec_more, 'front_rec_less': front_rec_less, 'back_rec_more': back_rec_more, 'back_rec_less': back_rec_less, } }) # 标记模块完成 complete_event = Event( event_id=int(time.time()), type=EventType.MODULE_COMPLETE, source=self.module_name, target='pool' ) event_center.publish(complete_event) def _update_dynamic_text(self, text: str): dialog = app.dialog_manager.active_dialogs.get(self.module_name) if dialog and hasattr(dialog, 'dynamic_text'): dialog.dynamic_text.insert('end', text) ==================== 趋势分析模块 ==================== class TrendAnalysisModule(BaseModule): def init(self, module_name: str): super().init(module_name) # 统一标签名格式(添加冒号) self.dynamic_data = { “和值”: “”, “质合比”: “”, “奇偶比”: “”, “断区推荐”: [], “连号推荐”: [], “冷热推荐”: [], “后区热号”: [], “后区冷号”: [], “趋势号”: [] } def _handle_module_run(self, event: Event): """处理趋势分析模块的运行事件""" logger.info(f"开始运行趋势分析模块: {event}") self.run() def run(self): dialog = app.dialog_manager.active_dialogs.get(self.module_name) if not dialog: return # 更新动态活动区显示 self._update_dynamic_text("开始趋势分析...\n") # 模拟分析过程 time.sleep(1) # 生成分析结果 (模拟) sum_value = random.randint(60, 125) prime_ratio = f"{random.randint(1, 5)}:{random.randint(1, 5)}" odd_even_ratio = f"{random.randint(1, 5)}:{random.randint(1, 5)}" zone_rec = random.choice(["一区", "二区", "三区", "四区", "五区", "六区", "七区"]) consec_rec = random.sample(range(1, 36), 2) hot_rec = sorted(random.sample(range(1, 13), 2)) cold_rec = sorted(random.sample(range(1, 13), 2)) trend_rec = sorted(random.sample(range(1, 36), 5)) hot_cold_rec = { 'hot': sorted(random.sample(range(1, 36), 5)), 'cold': sorted(random.sample(range(1, 36), 5)) } # 更新界面显示 result_text = f""" 趋势分析完成: 和值推荐: {sum_value} 质合比: {prime_ratio} 奇偶比: {odd_even_ratio} 断区推荐: {zone_rec} 连号推荐: {consec_rec} 后区热号: {hot_rec} 后区冷号: {cold_rec} 趋势号: {trend_rec} """ self._update_dynamic_text(result_text) # 提交数据到号码池 self._submit_data({ 'recommendations': { 'sum_rec': sum_value, 'prime_rec': prime_ratio, 'odd_even_rec': odd_even_ratio, 'zone_rec': zone_rec, 'consec_rec': consec_rec, 'hot_rec': hot_rec, 'cold_rec': cold_rec, 'trend_rec': trend_rec, 'hot_cold_rec': hot_cold_rec }, 'trend_data': { 'hot_numbers': hot_rec, 'cold_numbers': cold_rec, 'trend_numbers': trend_rec } }) # 标记模块完成 complete_event = Event( event_id=int(time.time()), type=EventType.MODULE_COMPLETE, source=self.module_name, target='pool' ) event_center.publish(complete_event) def _update_dynamic_text(self, text: str): dialog = app.dialog_manager.active_dialogs.get(self.module_name) if dialog and hasattr(dialog, 'dynamic_text'): dialog.dynamic_text.insert('end', text) ==================== 数字生成模块 ==================== class NumberGenerationModule(BaseModule): def init(self, module_name: str): super().init(module_name) # 统一标签名格式(添加冒号) self.dynamic_data = { “胆码”: { “前区”: [], “后区”: [] }, “推荐5注号码”: { “1”: “”, “2”: “”, “3”: “”, “4”: “”, “5”: “” } } def _handle_module_run(self, event: Event): """处理数字生成模块的运行事件""" logger.info(f"开始运行数字生成模块: {event}") self.run() def run(self): dialog = app.dialog_manager.active_dialogs.get(self.module_name) if not dialog: return # 获取胆码数据 front_dan = dialog.content_frame.front_dan_entry.get() back_dan = dialog.content_frame.back_dan_entry.get() # 在动态活动区显示分析过程 self._update_dynamic_text(f"开始数字生成...\n胆码数据: 前区:{front_dan}, 后区:{back_dan}\n") # 生成号码 generated_numbers = self._generate_numbers(front_dan, back_dan) # 更新动态活动区显示 result_text = "生成的5注号码:\n" for num, nums in enumerate(generated_numbers, 1): result_text += f"{num}: 前区:{nums['front']} 后区:{nums['back']}\n" self._update_dynamic_text(result_text) # 提交数据到号码池 self._submit_data({ 'generated_numbers': generated_numbers }) # 标记模块完成 complete_event = Event( event_id=int(time.time()), type=EventType.MODULE_COMPLETE, source=self.module_name, target='pool' ) event_center.publish(complete_event) def _update_dynamic_text(self, text: str): """更新动态区文本""" dialog = getattr(self, 'dialog', None) if dialog and hasattr(dialog, 'dynamic_text'): dialog.dynamic_text.insert('end', text) @staticmethod def _generate_numbers(front_dan: str, back_dan: str): """使用胆码生成号码""" # 这里实现具体的号码生成逻辑 # 返回生成的号码列表 generated_numbers = [] for _ in range(5): front_nums = sorted(random.sample(range(1, 36), 5)) back_nums = sorted(random.sample(range(1, 13), 2)) generated_numbers.append({ 'front': front_nums, 'back': back_nums }) return generated_numbers ==================== 主界面修改 ==================== class MainInterface: def init(self, root: Tk, pool: ‘NumberPool’): self.root = root self.pool = pool self.left_panel = None self.center_paned = None # 修正变量名 self.right_panel = None self.core_vars = {} self.pool_vars = {} self.status_var = StringVar() self.dynamic_text = None self.current_module = None self._setup_ui() self._setup_event_handlers() self.module_instances = {} self.exclude_front_entries = [] self.exclude_back_entries = [] self.front_dan_entries = [] self.back_dan_entries = [] # 初始化结果文本控件 self.result_text = None # 初始化排除号码变量 self.exclude_front_var = StringVar() self.exclude_back_var = StringVar() self.recommend_front_var = StringVar() self.recommend_back_var = StringVar() # 初始化模块内容框架 self.dynamic_content = None self.module_content_frame = None # 新增模块ID属性 self.module_ids = { ‘input_analysis’: GlobalConfig.MODULE1_ID, ‘combination_analysis’: GlobalConfig.MODULE2_ID, ‘follow_analysis’: GlobalConfig.MODULE3_ID, ‘trend_analysis’: GlobalConfig.MODULE4_ID, ‘number_generation’: GlobalConfig.MODULE5_ID, } # 模块标签定义 self.labels = { ‘input_analysis’: [ # 修正为小写 “排除号码:”, “前区:”, “后区:”, “推荐号码:”, “前区:”, “后区:”, ], ‘combination_analysis’: [ “前数字频:”, “前数字缺:”, “后数字频:”, “后数字缺:”, “前频繁推:”, “后低频推:”, “生组合数:”, “未组合码:” ], ‘follow_analysis’: [ “前推荐多:”, “前推荐少:”, “后推荐多:”, “后推荐少:” ], ‘trend_analysis’: [ “和值:”, “质合比:”, “奇偶比:”, “断区推荐:”, “连号推荐:”, “冷热推荐:”, “后区热号:”, “后区冷号:”, “趋势号:” ], ‘number_generation’: [ # 修正为小写 “胆码:”, “前区:”, “后区:”, “推荐5注号码:”, “1:”, “”, “2:”, “”, “3:”, “”, “4:”, “”, “5:”, “” ], } # 初始化所有模块的条目引用 self.front_dan_entry = None self.back_dan_entry = None self.result_text = None self.exclude_front_entry = None self.exclude_back_entry = None self.front_entry = None self.back_entry = None def _setup_event_handlers(self): """初始化事件处理器""" event_center.subscribe(EventType.MODULE_COMPLETE, self._handle_module_complete) event_center.subscribe(EventType.UI_UPDATE, self._handle_ui_update) event_center.subscribe(EventType.EXCLUDE_NUMBERS, self._handle_exclude_numbers) def _setup_ui(self): self.root.title(f"大乐透智能分析平台 - {GlobalConfig.VERSION}") self.root.geometry("1400x800") # 添加主标题 title_frame = Frame(self.root) title_frame.pack(fill='x', pady=5) Label(title_frame, text="大乐透智能分析平台", font=('微软雅黑', 16, 'bold')).pack(expand=True) # 主容器 - 三栏布局 main_container = PanedWindow(self.root, orient=HORIZONTAL, sashrelief=RAISED, sashwidth=5) main_container.pack(fill='both', expand=True, padx=5, pady=(0, 5)) # 左侧面板 self.left_panel = Frame(main_container, width=200, bg="#eaeaea") main_container.add(self.left_panel, minsize=150, stretch="never") # 中间内容区 self.center_paned = PanedWindow(main_container, orient=VERTICAL, sashrelief=RAISED, sashwidth=5) main_container.add(self.center_paned, minsize=500, stretch="always") # 右侧面板 self.right_panel = Frame(main_container, width=700, bg="#f5f5f5") main_container.add(self.right_panel, minsize=250, stretch="never") # 初始化各区域 self._setup_left_panel() self._setup_center_area() self._setup_right_panel() def _setup_left_panel(self): """初始化左侧模块按钮区""" module_names = { 'input_analysis': '1. 输入分析', 'combination_analysis': '2. 组合分析', 'follow_analysis': '3. 跟随分析', 'trend_analysis': '4. 趋势分析', 'number_generation': '5. 数字生成' } for module in GlobalConfig.MODULES: Button( self.left_panel, text=module_names[module], width=18, command=lambda m=module: self._on_module_button_click(m) ).pack(pady=3, padx=5, ipady=3) def _setup_center_area(self): """设置中间区域布局,分为上下两部分""" # 上半部分 - 核心区 (固定高度) self.core_frame = Frame(self.center_paned, bd=1, relief='solid') self.center_paned.add(self.core_frame, minsize=150, stretch="never") # 核心区内容 Label(self.core_frame, text="核心区", font=('微软雅黑', 12, 'bold')).pack(anchor='w', padx=5, pady=2) # 核心数据展示 self.core_vars = { 'front_area': StringVar(), 'back_area': StringVar(), 'front_hot': StringVar(), 'front_cold': StringVar(), 'back_hot': StringVar(), 'back_cold': StringVar() } for label, var_name in [ ("前区:", 'front_area'), ("后区:", 'back_area'), ("前区热号:", 'front_hot'), ("前区冷号:", 'front_cold'), ("后区热号:", 'back_hot'), ("后区冷号:", 'back_cold') ]: frame = Frame(self.core_frame) frame.pack(fill='x', padx=5, pady=2) Label(frame, text=label, width=10, anchor='w').pack(side='left') entry_container = Frame(frame) entry_container.pack(side='left', fill='x', expand=True) entry = Entry(entry_container, textvariable=self.core_vars[var_name], font=('微软雅黑', 10), state='readonly', readonlybackground='#f0f0f0', relief='sunken', bd=1) entry.pack(fill='x', expand=True) # 下半部分 - 动态区 self.dynamic_frame = Frame(self.center_paned, bd=1, relief='solid') self.center_paned.add(self.dynamic_frame, minsize=200, stretch="always") # 主容器使用Grid布局 self.dynamic_container = Frame(self.dynamic_frame) self.dynamic_container.pack(fill='both', expand=True) self.dynamic_container.grid_rowconfigure(0, weight=1) self.dynamic_container.grid_columnconfigure(0, weight=1) # 操作按钮放在右下角 self.btn_frame = Frame(self.dynamic_container) self.btn_frame.grid(row=1, column=0, sticky='se', pady=5, padx=5) Button(self.btn_frame, text="运行", width=8, command=self._run_current_module).pack(side='left', padx=2) Button(self.btn_frame, text="清除", width=8, command=self._clear_dynamic_content).pack(side='left', padx=2) Button(self.btn_frame, text="保存", width=8, command=self._save_dynamic_content).pack(side='left', padx=2) Button(self.btn_frame, text="刷新", width=8, command=self._refresh_dynamic).pack(side='left', padx=2) # 模块内容容器 - 确保先创建 self.module_content_frame = Frame(self.dynamic_container) self.module_content_frame.grid(row=0, column=0, sticky='nsew') # 初始化默认内容 self._init_default_dynamic_content() def _init_default_dynamic_content(self): """初始化默认动态区内容""" # 清除现有内容 for widget in self.module_content_frame.winfo_children(): widget.destroy() # 创建新内容 self.dynamic_content = Frame(self.module_content_frame) self.dynamic_content.pack(fill='both', expand=True) Label(self.dynamic_content, text="请从左侧选择分析模块", font=('微软雅黑', 12)).pack(expand=True, pady=50) def _on_module_button_click(self, module: str): """模块按钮点击事件处理""" self.status_var.set(f"打开 {module} 模块...") self.current_module = module # 确保 module_content_frame 存在且可操作 if not hasattr(self, 'module_content_frame') or self.module_content_frame is None: self.module_content_frame = Frame(self.dynamic_container) self.module_content_frame.grid(row=0, column=0, sticky='nsew') self._init_default_dynamic_content() logging.warning("module_content_frame 已紧急初始化") try: # 1️⃣ 彻底清除旧内容(包括残留按钮) for widget in self.module_content_frame.winfo_children(): widget.destroy() self.module_content_frame.update_idletasks() # 强制更新布局 # 2️⃣ 创建新内容容器 content_frame = Frame(self.module_content_frame) content_frame.pack(fill='both', expand=True) # 3️⃣ 根据模块类型创建具体内容 if module == "input_analysis": self._create_input_analysis_content(content_frame) elif module == "combination_analysis": self._create_combination_analysis_content(content_frame) elif module == "follow_analysis": self._create_follow_analysis_content(content_frame) elif module == "trend_analysis": self._create_trend_analysis_content(content_frame) elif module == "number_generation": self._create_number_generation_content(content_frame) # 4️⃣ 创建按钮容器(带唯一标识) # 先销毁已存在的按钮容器(严格检查类型和标记) for widget in self.module_content_frame.winfo_children(): if isinstance(widget, Frame) and hasattr(widget, 'is_button_container'): widget.destroy() # 创建新按钮容器并标记 btn_frame = Frame(self.module_content_frame) btn_frame.is_button_container = True # 标记为按钮容器 btn_frame.pack(side='bottom', fill='x', pady=5) btn_frame.pack_propagate(False) # 防止内部组件影响容器大小 # 按钮容器内部布局 btn_container = Frame(btn_frame) btn_container.pack(side='right') # 创建按钮(使用lambda时绑定当前模块状态) Button(btn_container, text="运行", width=8, command=lambda m=module: self._run_module(m)).pack(side='left', padx=2) Button(btn_container, text="清除", width=8, command=lambda m=module: self._clear_module_data(m)).pack(side='left', padx=2) Button(btn_container, text="保存", width=8, command=lambda m=module: self._save_module_data(m)).pack(side='left', padx=2) Button(btn_container, text="刷新", width=8, command=lambda m=module: self._on_module_button_click(m)).pack(side='left', padx=2) except Exception as e: # 5️⃣ 严重异常处理(强制重建界面) logging.critical(f"模块切换异常: {str(e)}", exc_info=True) if hasattr(self, 'module_content_frame'): self.module_content_frame.destroy() self.module_content_frame = Frame(self.dynamic_container) self.module_content_frame.grid(row=0, column=0, sticky='nsew') self._init_default_dynamic_content() def _run_current_module(self): """运行当前模块""" if self.current_module: self._run_module(self.current_module) def _clear_dynamic_content(self): """清除动态区内容""" if self.current_module: self._clear_module_data(self.current_module) # 额外确保清除结果文本框(如果存在) if hasattr(self, 'result_text') and self.result_text: self.result_text.delete(1.0, 'end') def _save_dynamic_content(self): """保存动态区内容""" if self.current_module: self._save_module_data(self.current_module) else: messagebox.showinfo("提示", "请先选择并运行一个模块") def _refresh_dynamic(self): """刷新动态区内容""" if self.current_module: self._on_module_button_click(self.current_module) else: messagebox.showinfo("提示", "请先选择一个模块") def _organize_data(self): """整理号码池数据""" try: # 发布整理事件 event = Event( event_id=int(time.time()), type=EventType.ORGANIZE_DATA, source='main_ui', target='pool' ) event_center.publish(event) self.status_var.set("号码池数据已整理") except Exception as e: messagebox.showerror("整理失败", str(e)) logging.error(f"整理数据失败: {str(e)}", exc_info=True) def _freeze_data(self): """冻结号码池数据""" try: # 发布冻结事件 event = Event( event_id=int(time.time()), type=EventType.FREEZE_DATA, source='main_ui', target='pool' ) event_center.publish(event) self.status_var.set("号码池数据已冻结") except Exception as e: messagebox.showerror("冻结失败", str(e)) logging.error(f"冻结数据失败: {str(e)}", exc_info=True) def _run_module(self, module: str): """运行指定模块""" if module == "input_analysis": # 获取排除号码 exclude_front = self.exclude_front_entry.get() exclude_back = self.exclude_back_entry.get() # 发布排除号码事件 exclude_event = Event( event_id=int(time.time()), type=EventType.EXCLUDE_NUMBERS, source='main_ui', target='pool', data={ 'exclude_front': exclude_front, 'exclude_back': exclude_back } ) event_center.publish(exclude_event) # 在结果文本中记录 self.result_text.insert('end', f"已设置排除号码: 前区 {exclude_front}, 后区 {exclude_back}\n") # 发布模块运行事件 run_event = Event( event_id=int(time.time()), type=EventType.MODULE_RUN, source='main_ui', target=module ) event_center.publish(run_event) def _create_ui_element(self, parent, label_text): """创建统一的UI元素(与核心区对齐)""" frame = Frame(parent) frame.pack(fill='x', pady=2) # 标签固定宽度与核心区对齐 Label(frame, text=label_text, width=10, anchor='w').pack(side='left') # 条目容器 - 宽度与核心区对齐 entry_container = Frame(frame) entry_container.pack(side='left', fill='x', expand=True) return entry_container def _setup_right_panel(self): """设置右侧号码池布局""" # 号码池标题 pool_title_frame = Frame(self.right_panel) pool_title_frame.pack(fill='x', pady=5) Label(pool_title_frame, text="号码池", font=('微软雅黑', 12, 'bold')).pack(anchor='w') # 号码池内容区(添加边框和2px内边距) pool_content = Frame(self.right_panel, bd=1, relief='solid', padx=2, pady=2) pool_content.pack(fill='both', expand=True, padx=5, pady=5) # 创建Canvas和Scrollbar canvas = Canvas(pool_content, highlightthickness=0) scrollbar = Scrollbar(pool_content, orient="vertical", command=canvas.yview) scrollable_frame = Frame(canvas) scrollable_frame.bind( "<Configure>", lambda e: canvas.configure(scrollregion=canvas.bbox("all")) ) canvas.create_window((0, 0), window=scrollable_frame, anchor="nw") canvas.configure(yscrollcommand=scrollbar.set) # 号码池项目 - 优化布局和样式(带2px右边距) for label, var_name, row_id in GlobalConfig.UI_CONFIG: frame = Frame(scrollable_frame) frame.grid(row=row_id, column=0, sticky='ew', padx=0, pady=1) # 移除水平padding # 左侧标签(固定宽度8字符) lbl = Label(frame, text=label, width=8, anchor='w') lbl.pack(side='left', padx=(0, 5)) # 标签右侧留5px间距 # 右侧输入框容器(带2px右边距) entry_container = Frame(frame) entry_container.pack(side='left', fill='x', expand=True, padx=(0, 2)) var = StringVar() self.pool_vars[var_name] = var entry = Entry(entry_container, textvariable=var, font=('微软雅黑', 9), state='readonly', readonlybackground='#f0f0f0', relief='sunken', bd=1) entry.pack(fill='x', expand=True) canvas.pack(side="left", fill="both", expand=True) scrollbar.pack(side="right", fill="y") # 底部按钮区 btn_frame = Frame(self.right_panel) btn_frame.pack(fill='x', pady=5) Button(btn_frame, text="整理", width=10, command=self._organize_data).pack(side='left', padx=5) Button(btn_frame, text="冻结", width=10, command=self._freeze_data).pack(side='left', padx=5) Button(btn_frame, text="导出", width=10).pack(side='left', padx=5) def _create_input_analysis_content(self, parent: Frame): """创建输入分析模块内容 - 增加模块ID标识""" # 主容器使用Grid布局 main_frame = Frame(parent) main_frame.pack(fill='both', expand=True) # 添加模块ID标识(右上角) module_id_label = Label(main_frame, text=f"模块ID: {GlobalConfig.MODULE1_ID}", font=('微软雅黑', 8), fg='gray') module_id_label.pack(anchor='ne', padx=10, pady=5) # === 排除号码区 === exclude_frame = LabelFrame(main_frame, text=" 排除号码 ", font=('微软雅黑', 12, 'bold')) exclude_frame.pack(fill='x', padx=20, pady=10, ipady=5) # 使用预定义的标签文本 labels = self.labels['input_analysis'] # 排除号码标签 Label(exclude_frame, text=labels[0], font=('微软雅黑', 10, 'bold')).pack(anchor='w', padx=10, pady=(5, 0)) # 前区排除 Label(exclude_frame, text=labels[1], font=('微软雅黑', 10)).pack(anchor='w', padx=10, pady=5) front_entries_frame = Frame(exclude_frame) front_entries_frame.pack(fill='x', padx=10, pady=5) self.exclude_front_entries = [] for i in range(10): # 10个前区输入框 entry_frame = Frame(front_entries_frame) entry_frame.pack(side='left', padx=2) Label(entry_frame, text=f"{i + 1}:").pack(side='left') entry = Entry(entry_frame, width=3, font=('微软雅黑', 10)) entry.pack(side='left') # 添加验证函数 - 前区限制1-35 entry.config(validate="key", validatecommand=( entry.register(lambda text: self._validate_number(text, 1, 35)), '%P')) entry.bind("<KeyRelease>", self._auto_format_entry) entry.bind("<Left>", lambda e, d=-1: self._navigate_entry(e, d)) entry.bind("<Right>", lambda e, d=1: self._navigate_entry(e, d)) self.exclude_front_entries.append(entry) # 后区排除 Label(exclude_frame, text=labels[2], font=('微软雅黑', 10)).pack(anchor='w', padx=10, pady=(10, 5)) back_entries_frame = Frame(exclude_frame) back_entries_frame.pack(fill='x', padx=10, pady=5) self.exclude_back_entries = [] for i in range(10): # 10个后区输入框 entry_frame = Frame(back_entries_frame) entry_frame.pack(side='left', padx=2) Label(entry_frame, text=f"{i + 1}:").pack(side='left') entry = Entry(entry_frame, width=3, font=('微软雅黑', 10)) entry.pack(side='left') # 添加验证函数 - 后区限制1-12 entry.config(validate="key", validatecommand=( entry.register(lambda text: self._validate_number(text, 1, 12)), '%P')) entry.bind("<KeyRelease>", self._auto_format_entry) entry.bind("<Left>", lambda e, d=-1: self._navigate_entry(e, d)) entry.bind("<Right>", lambda e, d=1: self._navigate_entry(e, d)) self.exclude_back_entries.append(entry) # 加载保存的数据 - 关键修改点 # 加载保存的数据 - 使用正确的路径常量 if os.path.exists(GlobalConfig.MODULE1_SAVE_PATH): # 修复路径变量名 try: with open(GlobalConfig.MODULE1_SAVE_PATH, 'r', encoding='utf-8') as f: # 修复路径变量名 saved_data = json.load(f) # 恢复前区排除号码 if 'exclude_front' in saved_data: for i, num in enumerate(saved_data['exclude_front']): if i < len(self.exclude_front_entries): formatted_num = f"{int(num):02d}" if num.isdigit() else num self.exclude_front_entries[i].delete(0, 'end') self.exclude_front_entries[i].insert(0, formatted_num) # 恢复后区排除号码 if 'exclude_back' in saved_data: for i, num in enumerate(saved_data['exclude_back']): if i < len(self.exclude_back_entries): formatted_num = f"{int(num):02d}" if num.isdigit() else num self.exclude_back_entries[i].delete(0, 'end') self.exclude_back_entries[i].insert(0, formatted_num) except Exception as e: logging.error(f"加载保存数据失败: {str(e)}") # 确保保存了排除号码输入框的引用 self.exclude_front_entry = self.exclude_front_entries[0] # 保存第一个前区排除输入框 self.exclude_back_entry = self.exclude_back_entries[0] # 保存第一个后区排除输入框 # === 推荐号码区 === recommend_frame = LabelFrame(main_frame, text=" 推荐号码 ", font=('微软雅黑', 12, 'bold')) recommend_frame.pack(fill='x', padx=20, pady=10, ipady=5) # 推荐号码标签 Label(recommend_frame, text=labels[3], font=('微软雅黑', 10, 'bold')).pack(anchor='w', padx=10, pady=(5, 0)) # 前区推荐 front_rec_frame = Frame(recommend_frame) front_rec_frame.pack(fill='x', padx=10, pady=5) Label(front_rec_frame, text=labels[4], font=('微软雅黑', 10)).pack(side='left') self.recommend_front_var = StringVar() Entry(front_rec_frame, textvariable=self.recommend_front_var, state='readonly', font=('微软雅黑', 10), readonlybackground='#f0f5f0').pack(side='left', fill='x', expand=True, padx=5) # 后区推荐 back_rec_frame = Frame(recommend_frame) back_rec_frame.pack(fill='x', padx=10, pady=5) Label(back_rec_frame, text=labels[5], font=('微软雅黑', 10)).pack(side='left') self.recommend_back_var = StringVar() Entry(back_rec_frame, textvariable=self.recommend_back_var, state='readonly', font=('微软雅黑', 10), readonlybackground='#f0f5f0').pack(side='left', fill='x', expand=True, padx=5) # === 结果区 === result_frame = LabelFrame(main_frame, text=" 分析结果 ", font=('微软雅黑', 12, 'bold')) result_frame.pack(fill='both', expand=True, padx=20, pady=10, ipady=5) scrollbar = Scrollbar(result_frame) scrollbar.pack(side='right', fill='y') self.result_text = Text(result_frame, yscrollcommand=scrollbar.set, wrap='word', font=('微软雅黑', 10)) self.result_text.pack(fill='both', expand=True) scrollbar.config(command=self.result_text.yview) # 添加初始提示 self.result_text.insert('end', "请设置排除号码后点击'运行'按钮开始分析\n") # 强制刷新界面 self.root.update_idletasks() def _handle_entry_input(self, event): """处理输入框相关事件的总入口""" if event.keysym in ('Left', 'Right'): # 处理方向键导航 self._navigate_entry(event, 1 if event.keysym == 'Right' else -1) else: # 处理输入自动格式化 self._auto_format_entry(event) def _auto_format_entry(self, event): """ 自动格式化输入框内容 功能: 1. 自动将1-9的数字补零显示为01-09 2. 输入满2位后自动跳到下一个输入框 """ entry = event.widget current = entry.get().strip() if current.isdigit(): # 只处理数字输入 # 自动补零处理 formatted = self._format_number(current) if formatted != current: entry.delete(0, 'end') entry.insert(0, formatted) # 输入满2位后自动跳转 if len(current) == 2: self._focus_adjacent_entry(event.widget, 1) # 正向跳转 def _focus_adjacent_entry(self, current_entry, direction): """ 焦点跳转到相邻输入框 :param current_entry: 当前输入框控件 :param direction: 跳转方向 (1:下一个, -1:上一个) """ all_entries = self.exclude_front_entries + self.exclude_back_entries try: current_index = all_entries.index(current_entry) target_index = current_index + direction if 0 <= target_index < len(all_entries): all_entries[target_index].focus() except ValueError: pass def _navigate_entry(self, event, direction): """使用方向键在输入框间导航""" self._focus_adjacent_entry(event.widget, direction) def _format_number(self, num_str: str) -> str: """ 格式化数字为两位数 :param num_str: 输入的数字字符串 :return: 格式化后的两位数字符串 """ if not num_str.isdigit(): return num_str # 非数字不处理 num = int(num_str) if 1 <= num <= 9: # 1-9的数字补零 return f"0{num}" return str(num) if num > 0 else num_str # 保留0和负数原样 def _create_combination_analysis_content(self, parent: Frame): """创建组合分析模块的特定内容""" content_frame = Frame(parent) content_frame.pack(fill='both', expand=True, padx=10, pady=10) # 使用预定义的labels for label in self.labels['combination_analysis']: frame = Frame(content_frame) frame.pack(fill='x', pady=2) Label(frame, text=label, width=12, anchor='w', font=('微软雅黑', 10, 'bold')).pack(side='left') entry = Entry(frame, width=30, state='readonly', readonlybackground='#f0f0f0') entry.pack(side='left', padx=5) # 保存对控件的引用 var_name = label.replace(':', '').replace(' ', '') setattr(self, f"{var_name}_entry", entry) # 直接保存到实例变量 if var_name == "前区热号": self.front_hot_entry = entry elif var_name == "前数字频": self.front_freq_entry = entry elif var_name == "前频繁推": self.front_freq_rec_entry = entry elif var_name == "后区热号": self.back_hot_entry = entry elif var_name == "后数字频": self.back_freq_entry = entry elif var_name == "后低频推": self.back_infreq_rec_entry = entry # 结果显示区 result_frame = Frame(content_frame) result_frame.pack(fill='both', expand=True) scrollbar = Scrollbar(result_frame) scrollbar.pack(side='right', fill='y') self.result_text = Text(result_frame, yscrollcommand=scrollbar.set, wrap='word') self.result_text.pack(fill='both', expand=True) scrollbar.config(command=self.result_text.yview) def _create_follow_analysis_content(self, parent: Frame): """创建跟随分析模块的特定内容(修复版)""" content_frame = Frame(parent) content_frame.pack(fill='both', expand=True, padx=10, pady=10) # 使用预定义的labels(确保不重复创建按钮) for label in self.labels['follow_analysis']: frame = Frame(content_frame) frame.pack(fill='x', pady=2) Label(frame, text=label, width=12, anchor='w', font=('微软雅黑', 10, 'bold')).pack(side='left') entry = Entry(frame, width=30, state='readonly', readonlybackground='#f0f0f0') entry.pack(side='left', padx=5) # 保存控件引用 var_name = label.replace(':', '').replace(' ', '') setattr(self, f"{var_name}_entry", entry) # 结果显示区(不包含按钮) result_frame = Frame(content_frame) result_frame.pack(fill='both', expand=True) scrollbar = Scrollbar(result_frame) scrollbar.pack(side='right', fill='y') self.result_text = Text(result_frame, yscrollcommand=scrollbar.set, wrap='word') self.result_text.pack(fill='both', expand=True) scrollbar.config(command=self.result_text.yview) def _create_trend_analysis_content(self, parent: Frame): """创建趋势分析模块的特定内容""" content_frame = Frame(parent) content_frame.pack(fill='both', expand=True, padx=10, pady=10) for label in self.labels['trend_analysis']: frame = Frame(content_frame) frame.pack(fill='x', pady=2) Label(frame, text=label, width=12, anchor='w', font=('微软雅黑', 10, 'bold')).pack(side='left') entry = Entry(frame, width=30, state='readonly', readonlybackground='#f0f0f0') entry.pack(side='left', padx=5) var_name = label.replace(':', '').replace(' ', '') setattr(self, f"{var_name}_entry", entry) # 直接保存到实例变量 if var_name == "和值": self.sum_value_entry = entry elif var_name == "质合比": self.prime_ratio_entry = entry elif var_name == "奇偶比": self.odd_even_ratio_entry = entry elif var_name == "断区推荐": self.zone_rec_entry = entry elif var_name == "连号推荐": self.consec_rec_entry = entry elif var_name == "冷热推荐": self.hot_cold_rec_entry = entry elif var_name == "后区热号": self.hot_rec_entry = entry elif var_name == "后区冷号": self.cold_rec_entry = entry elif var_name == "趋势号": self.trend_rec_entry = entry # 结果显示区 result_frame = Frame(content_frame) result_frame.pack(fill='both', expand=True) scrollbar = Scrollbar(result_frame) scrollbar.pack(side='right', fill='y') self.result_text = Text(result_frame, yscrollcommand=scrollbar.set, wrap='word') self.result_text.pack(fill='both', expand=True) scrollbar.config(command=self.result_text.yview) def _create_number_generation_content(self, parent: Frame): """创建数字生成模块的动态内容""" content_frame = Frame(parent) content_frame.pack(fill='both', expand=True, padx=10, pady=10) # 胆码输入区 dan_frame = Frame(content_frame) dan_frame.pack(fill='x', pady=5) # 前区胆码 front_dan_frame = Frame(dan_frame) front_dan_frame.pack(fill='x') Label(front_dan_frame, text="前区胆码:").pack(side='left') self.front_dan_entries = [] for i in range(5): entry = Entry(front_dan_frame, width=3) entry.pack(side='left', padx=2) self.front_dan_entries.append(entry) self.front_dan_entry = self.front_dan_entries[0] # 保存第一个条目引用 # 后区胆码 back_dan_frame = Frame(dan_frame) back_dan_frame.pack(fill='x') Label(back_dan_frame, text="后区胆码:").pack(side='left') self.back_dan_entries = [] for i in range(5): entry = Entry(back_dan_frame, width=3) entry.pack(side='left', padx=2) self.back_dan_entries.append(entry) self.back_dan_entry = self.back_dan_entries[0] # 保存第一个条目引用 # 生成的号码显示区 generated_frame = Frame(content_frame) generated_frame.pack(fill='x', pady=5) Label(generated_frame, text="生成号码:").pack(anchor='w') self.generated_labels = [] for i in range(1, 6): frame = Frame(generated_frame) frame.pack(fill='x') Label(frame, text=f"{i}. ").pack(side='left') label = Label(frame, text="", width=30, anchor='w') label.pack(side='left') self.generated_labels.append(label) # 结果显示区 result_frame = Frame(content_frame) result_frame.pack(fill='both', expand=True) scrollbar = Scrollbar(result_frame) scrollbar.pack(side='right', fill='y') self.result_text = Text(result_frame, yscrollcommand=scrollbar.set, wrap='word') self.result_text.pack(fill='both', expand=True) scrollbar.config(command=self.result_text.yview) def _run_module(self, module: str): """运行指定模块(增加模块ID标识)""" if module == "input_analysis": # 获取并格式化排除号码 exclude_front_list = [] for entry in self.exclude_front_entries: num = entry.get() if num: # 只处理非空输入 formatted = self._format_number(num) exclude_front_list.append(formatted) exclude_back_list = [] for entry in self.exclude_back_entries: num = entry.get() if num: # 只处理非空输入 formatted = self._format_number(num) exclude_back_list.append(formatted) exclude_front = ' '.join(exclude_front_list) exclude_back = ' '.join(exclude_back_list) # 发布排除号码事件(包含模块ID) exclude_event = Event( event_id=int(time.time()), type=EventType.EXCLUDE_NUMBERS, source='main_ui', target='pool', data={ 'exclude_front': exclude_front, 'exclude_back': exclude_back, 'module_id': GlobalConfig.MODULE1_ID # 添加模块ID } ) event_center.publish(exclude_event) # 在结果文本中记录 self.result_text.insert('end', f"已设置排除号码: 前区 {exclude_front}, 后区 {exclude_back}\n") # 发布模块运行事件 run_event = Event( event_id=int(time.time()), type=EventType.MODULE_RUN, source='main_ui', target=module, data={ 'exclude_front': exclude_front, 'exclude_back': exclude_back } ) event_center.publish(run_event) # 生成推荐号码 self._generate_recommend_numbers(exclude_front, exclude_back) def _generate_recommend_numbers(self, exclude_front: str, exclude_back: str): """生成推荐号码(示例逻辑)""" # 实际应用中应调用分析模块生成推荐号码 # 这里简化为生成随机推荐号码 import random # 前区号码范围1-35 all_front = [str(idx) for idx in range(1, 36)] exclude_front_list = exclude_front.split() if exclude_front else [] available_front = [num for num in all_front if num not in exclude_front_list] # 后区号码范围1-12 all_back = [str(idx) for idx in range(1, 13)] exclude_back_list = exclude_back.split() if exclude_back else [] available_back = [num for num in all_back if num not in exclude_back_list] # 随机选择5个前区号码 if len(available_front) >= 5: recommend_front = random.sample(available_front, 5) else: recommend_front = random.sample(all_front, 5) # 随机选择2个后区号码 if len(available_back) >= 2: recommend_back = random.sample(available_back, 2) else: recommend_back = random.sample(all_back, 2) # 更新推荐号码显示 self.recommend_front_var.set(' '.join(sorted(recommend_front, key=int))) self.recommend_back_var.set(' '.join(sorted(recommend_back, key=int))) # 在结果文本中记录 self.result_text.insert('end', f"生成推荐号码: 前区 {self.recommend_front_var.get()}, 后区 {self.recommend_back_var.get()}\n") # 更新号码池 self._update_pool_with_recommendations(self.recommend_front_var.get(), self.recommend_back_var.get()) def update_recommendations(self, fronts: List[int], backs: List[int]): """更新推荐号码显示(使用号码池路径保存)""" formatted_fronts = [str(num).zfill(2) for num in fronts] formatted_backs = [str(num).zfill(2) for num in backs] self.recommend_front_var.set(' '.join(formatted_fronts)) self.recommend_back_var.set(' '.join(formatted_backs)) # 保存到号码池文件 try: pool_data = { 'recommended_fronts': formatted_fronts, 'recommended_backs': formatted_backs, 'module_id': GlobalConfig.MODULE1_ID, 'timestamp': time.strftime("%Y-%m-%d %H:%M:%S") } with open(GlobalConfig.POOL_SAVE_PATH, 'w', encoding='utf-8') as f: json.dump(pool_data, f, indent=2, ensure_ascii=False) logging.info(f"推荐号码已保存到号码池: {GlobalConfig.POOL_SAVE_PATH}") except Exception as e: logging.error(f"保存号码池失败: {str(e)}") def _clear_module_data(self, module: str): """清除模块数据""" if module == "input_analysis": if hasattr(self, 'front_entry') and self.front_entry: self.front_entry.delete(0, 'end') if hasattr(self, 'back_entry') and self.back_entry: self.back_entry.delete(0, 'end') if hasattr(self, 'exclude_front_entry') and self.exclude_front_entry: self.exclude_front_entry.delete(0, 'end') if hasattr(self, 'exclude_back_entry') and self.exclude_back_entry: self.exclude_back_entry.delete(0, 'end') if hasattr(self, 'recommend_front_var'): self.recommend_front_var.set('') if hasattr(self, 'recommend_back_var'): self.recommend_back_var.set('') if hasattr(self, 'result_text') and self.result_text: self.result_text.delete(1.0, 'end') elif module == "combination_analysis": if hasattr(self, 'front_hot_entry') and self.front_hot_entry: self.front_hot_entry.delete(0, 'end') if hasattr(self, 'front_freq_entry') and self.front_freq_entry: self.front_freq_entry.delete(0, 'end') if hasattr(self, 'front_freq_rec_entry') and self.front_freq_rec_entry: self.front_freq_rec_entry.delete(0, 'end') if hasattr(self, 'back_hot_entry') and self.back_hot_entry: self.back_hot_entry.delete(0, 'end') if hasattr(self, 'back_freq_entry') and self.back_freq_entry: self.back_freq_entry.delete(0, 'end') if hasattr(self, 'back_infreq_rec_entry') and self.back_infreq_rec_entry: self.back_infreq_rec_entry.delete(0, 'end') if hasattr(self, 'result_text') and self.result_text: self.result_text.delete(1.0, 'end') if hasattr(self, 'result_text') and self.result_text: self.result_text.delete(1.0, 'end') if hasattr(self, 'front_entry') and self.front_entry: self.front_entry.delete(0, 'end') if hasattr(self, 'back_entry') and self.back_entry: self.back_entry.delete(0, 'end') if hasattr(self, 'exclude_front_entry') and self.exclude_front_entry: self.exclude_front_entry.delete(0, 'end') if hasattr(self, 'exclude_back_entry') and self.exclude_back_entry: self.exclude_back_entry.delete(0, 'end') def _save_module_data(self, module: str): """保存模块数据(修复路径问题)""" try: data = {} if module == "input_analysis": # 收集排除号码 exclude_front_list = [] for entry in self.exclude_front_entries: num = entry.get() if num: exclude_front_list.append(num) exclude_back_list = [] for entry in self.exclude_back_entries: num = entry.get() if num: exclude_back_list.append(num) data = { 'exclude_front': exclude_front_list, 'exclude_back': exclude_back_list, 'module_id': GlobalConfig.MODULE1_ID, 'timestamp': time.strftime("%Y-%m-%d %H:%M:%S") } # 使用正确的路径常量 filename = GlobalConfig.MODULE1_SAVE_PATH # 修复路径变量名 with open(filename, 'w', encoding='utf-8') as f: json.dump(data, f, indent=2, ensure_ascii=False) messagebox.showinfo("保存成功", f"排除号码已保存到{filename}") elif module == "combination_analysis": # 其他模块的保存逻辑... pass elif module == "combination_analysis": data['front_hot'] = self.front_hot_entry.get() data['front_freq'] = self.front_freq_entry.get() data['front_freq_rec'] = self.front_freq_rec_entry.get() data['back_hot'] = self.back_hot_entry.get() data['back_freq'] = self.back_freq_entry.get() data['back_infreq_rec'] = self.back_infreq_rec_entry.get() data['result'] = self.result_text.get(1.0, 'end') # 其他模块数据收集... # 使用模块专用保存路径 filename = GlobalConfig.MODULE1_SAVE_PATH with open(filename, 'w', encoding='utf-8') as f: json.dump(data, f, indent=2, ensure_ascii=False) messagebox.showinfo("保存成功", f"排除号码已保存到{filename}") except Exception as e: messagebox.showerror("保存失败", str(e)) logging.error(f"保存数据失败: {str(e)}", exc_info=True) def _handle_exclude_numbers(self, event: Event): """处理排除号码事件""" if event.data: exclude_front = event.data.get('exclude_front', '') exclude_back = event.data.get('exclude_back', '') # 更新排除号码显示 self.exclude_front_entry.delete(0, 'end') self.exclude_front_entry.insert(0, exclude_front) self.exclude_back_entry.delete(0, 'end') self.exclude_back_entry.insert(0, exclude_back) # 在结果文本中记录 self.result_text.insert('end', f"收到排除号码: 前区 {exclude_front}, 后区 {exclude_back}\n") def _handle_module_complete(self, event: Event): self.status_var.set(f"{event.source} 模块运行完成") if event.source == "input_analysis" and hasattr(self, 'result_text') and self.result_text: # 更新推荐号码显示 if 'recommend_front' in event.data: self.recommend_front_var.set(event.data['recommend_front']) if 'recommend_back' in event.data: self.recommend_back_var.set(event.data['recommend_back']) # 在结果文本中记录 self.result_text.insert('end', f"\n{event.source} 模块已完成分析\n") self.result_text.insert('end', f"推荐号码: 前区 {self.recommend_front_var.get()}, 后区 {self.recommend_back_var.get()}\n") # 同步更新号码池 update_event = Event( event_id=int(time.time()), type=EventType.POOL_UPDATE, source='input_analysis', target='pool', data={ 'front_numbers': self.recommend_front_var.get(), 'back_numbers': self.recommend_back_var.get() } ) event_center.publish(update_event) self.result_text.insert('end', "号码池已同步更新\n") def _on_module_renovate(self, module: str): """刷新模块""" if module == self.current_module: self._on_module_button_click(module) def _handle_ui_update(self, event: Event): """处理UI更新事件""" if not event.data or 'update_type' not in event.data: return update_type = event.data['update_type'] data = event.data.get('data', {}) # 处理核心变量更新 if update_type == 'organized_data': # 确保所有核心变量已初始化 if not hasattr(self, 'core_vars'): self.core_vars = { 'front_area': StringVar(), 'back_area': StringVar(), 'front_hot': StringVar(), 'front_cold': StringVar(), 'back_hot': StringVar(), 'back_cold': StringVar() } # 更新界面变量 self.core_vars['front_area'].set(str(data.get('front_numbers', []))) self.core_vars['back_area'].set(str(data.get('back_numbers', []))) self.core_vars['front_hot'].set(str(data.get('front_hot', []))) self.core_vars['front_cold'].set(str(data.get('front_cold', []))) self.core_vars['back_hot'].set(str(data.get('back_hot', []))) self.core_vars['back_cold'].set(str(data.get('back_cold', []))) ==================== 主程序 ==================== def main(): try: root = Tk() root.geometry(“1200x800”) # 初始化核心组件 pool = NumberPool() app.main_ui = MainInterface(root, pool) # 初始化所有模块 modules = { 'input_analysis': InputAnalysisModule, 'combination_analysis': CombinationAnalysisModule, 'follow_analysis': FollowAnalysisModule, 'trend_analysis': TrendAnalysisModule, 'number_generation': NumberGenerationModule } # 先创建所有模块实例 module_instances = {} for name, cls in modules.items(): module_instances[name] = cls(name) time.sleep(0.1) # 确保顺序初始化 root.mainloop() except Exception as e: logging.error(f"系统启动失败: {str(e)}", exc_info=True) sys.exit(1) if name == “main”: main()
07-18
import streamlit as st import pandas as pd import numpy as np import matplotlib.pyplot as plt import seaborn as sns from pyspark.sql import SparkSession from pyspark.ml.feature import VectorAssembler, StringIndexer, OneHotEncoder from pyspark.ml import Pipeline from pyspark.ml.classification import LogisticRegression, DecisionTreeClassifier, RandomForestClassifier from pyspark.ml.evaluation import BinaryClassificationEvaluator, MulticlassClassificationEvaluator import joblib import os import time import warnings from io import BytesIO import platform from pathlib import Path def safe_path(path): “”“处理Windows长路径问题”“” if platform.system() == ‘Windows’: try: import ntpath return ntpath.realpath(path) except: return str(Path(path).resolve()) return path 忽略警告 warnings.filterwarnings(“ignore”) 设置中文字体 plt.rcParams[‘font.sans-serif’] = [‘SimHei’] plt.rcParams[‘axes.unicode_minus’] = False 页面设置 st.set_page_config( page_title=“精准营销系统”, page_icon=“📊”, layout=“wide”, initial_sidebar_state=“expanded” ) 自定义CSS样式 st.markdown(“”" <style> .stApp { background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%); font-family: 'Helvetica Neue', Arial, sans-serif; } .header { background: linear-gradient(90deg, #1a237e 0%, #283593 100%); color: white; padding: 1.5rem; border-radius: 0.75rem; box-shadow: 0 4px 12px rgba(0,0,0,0.1); margin-bottom: 2rem; } .card { background: white; border-radius: 0.75rem; padding: 1rem; margin-bottom: 1.5rem; box-shadow: 0 4px 12px rgba(0,0,0,0.08); transition: transform 0.3s ease; } .card:hover { transform: translateY(-5px); box-shadow: 0 6px 16px rgba(0,0,0,0.12); } .stButton button { background: linear-gradient(90deg, #3949ab 0%, #1a237e 100%) !important; color: white !important; border: none !important; border-radius: 0.5rem; padding: 0.75rem 1.5rem; font-size: 1rem; font-weight: 600; transition: all 0.3s ease; width: 100%; } .stButton button:hover { transform: scale(1.05); box-shadow: 0 4px 8px rgba(57, 73, 171, 0.4); } .feature-box { background: linear-gradient(135deg, #e3f2fd 0%, #bbdefb 100%); border-radius: 0.75rem; padding: 1.5rem; margin-bottom: 1.5rem; } .result-box { background: linear-gradient(135deg, #e8f5e9 0%, #c8e6c9 100%); border-radius: 0.75rem; padding: 1.5rem; margin-top: 1.5rem; } .model-box { background: linear-gradient(135deg, #fff3e0 0%, #ffe0b2 100%); border-radius: 0.75rem; padding: 1.5rem; margin-top: 1.5rem; } .stProgress > div > div > div { background: linear-gradient(90deg, #2ecc71 0%, #27ae60 100%) !important; } .metric-card { background: white; border-radius: 0.75rem; padding: 1rem; text-align: center; box-shadow: 0 4px 8px rgba(0,0,0,0.06); } .metric-value { font-size: 1.8rem; font-weight: 700; color: #1a237e; } .metric-label { font-size: 0.9rem; color: #5c6bc0; margin-top: 0.5rem; } .highlight { background: linear-gradient(90deg, #ffeb3b 0%, #fbc02d 100%); padding: 0.2rem 0.5rem; border-radius: 0.25rem; font-weight: 600; } .stDataFrame { border-radius: 0.75rem; box-shadow: 0 4px 8px rgba(0,0,0,0.06); } .convert-high { background-color: #c8e6c9 !important; color: #388e3c !important; font-weight: 700; } .convert-low { background-color: #ffcdd2 !important; color: #c62828 !important; font-weight: 600; } </style> “”", unsafe_allow_html=True) 创建Spark会话 def create_spark_session(): return SparkSession.builder .appName(“TelecomPrecisionMarketing”) .config(“spark.driver.memory”, “4g”) .config(“spark.executor.memory”, “4g”) .getOrCreate() 数据预处理函数 - 修改后 def preprocess_data(df): “”" 数据预处理函数 参数: df: 原始数据 (DataFrame) 返回: 预处理后的数据 (DataFrame) “”" # 1. 选择关键特征 - 使用实际存在的列名 available_features = [col for col in df.columns if col in [ ‘AGE’, ‘GENDER’, ‘ONLINE_DAY’, ‘TERM_CNT’, ‘IF_YHTS’, ‘MKT_STAR_GRADE_NAME’, ‘PROM_AMT_MONTH’, ‘is_rh_next’ # 目标变量 ]] # 确保目标变量存在 if 'is_rh_next' not in available_features: st.error("错误:数据集中缺少目标变量 'is_rh_next'") return df # 只保留需要的列 df = df[available_features].copy() # 2. 处理缺失值 # 数值特征用均值填充 numeric_cols = ['AGE', 'ONLINE_DAY', 'TERM_CNT', 'PROM_AMT_MONTH'] for col in numeric_cols: if col in df.columns: mean_val = df[col].mean() df[col].fillna(mean_val, inplace=True) # 分类特征用众数填充 categorical_cols = ['GENDER', 'MKT_STAR_GRADE_NAME', 'IF_YHTS'] for col in categorical_cols: if col in df.columns: mode_val = df[col].mode()[0] df[col].fillna(mode_val, inplace=True) # 3. 异常值处理(使用IQR方法) def handle_outliers(series): Q1 = series.quantile(0.25) Q3 = series.quantile(0.75) IQR = Q3 - Q1 lower_bound = Q1 - 1.5 * IQR upper_bound = Q3 + 1.5 * IQR return series.clip(lower_bound, upper_bound) for col in numeric_cols: if col in df.columns: df[col] = handle_outliers(df[col]) return df 标题区域 st.markdown(“”" <div class="header"> <h1 style='text-align: center; margin: 0;'>精准营销系统</h1> <p style='text-align: center; margin: 0.5rem 0 0; font-size: 1.1rem;'>基于机器学习的单宽转融预测</p> </div> """, unsafe_allow_html=True) 页面布局 col1, col2 = st.columns([1, 1.5]) 左侧区域 - 图片和简介 with col1: st.markdown(“”" 📱 智能营销系统 预测单宽带用户转化为融合套餐用户的可能性 “”", unsafe_allow_html=True) # 使用在线图片作为占位符 st.image("https://images.unsplash.com/photo-1551836022-d5d88e9218df?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=1200&q=80", caption="精准营销系统示意图", width=600) st.markdown(""" <div class="card"> <h4>📈 系统功能</h4> <ul> <li>用户转化可能性预测</li> <li>高精度机器学习模型</li> <li>可视化数据分析</li> <li>精准营销策略制定</li> </ul> </div> """, unsafe_allow_html=True) 右侧区域 - 功能选择 with col2: st.markdown(“”" 📋 请选择操作类型 您可以选择数据分析或使用模型进行预测 “”", unsafe_allow_html=True) # 功能选择 option = st.radio("", ["📊 数据分析 - 探索数据并训练模型", "🔍 预测分析 - 预测用户转化可能性"], index=0, label_visibility="hidden") # 数据分析部分 if "数据分析" in option: st.markdown(""" <div class="card"> <h3>数据分析与模型训练</h3> <p>上传数据并训练预测模型</p> </极客时间> """, unsafe_allow_html=True) # 上传训练数据 train_file = st.file_uploader("上传数据集 (CSV格式, GBK编码)", type=["csv"]) if train_file is not None: try: # 读取数据 train_data = pd.read_csv(train_file, encoding='GBK') # 显示数据预览 with st.expander("数据预览", expanded=True): st.dataframe(train_data.head()) col1, col2 = st.columns(2) col1.metric("总样本数", train_data.shape[0]) col2.metric("特征数量", train_data.shape[1] - 1) # 数据预处理 st.subheader("数据预处理") with st.spinner("数据预处理中..."): processed_data = preprocess_data(train_data) st.success("✅ 数据预处理完成") # 可视化数据分布 st.subheader("数据分布分析") # 目标变量分布 st.markdown("**目标变量分布 (is_rh_next)**") fig, ax = plt.subplots(figsize=(8, 5)) sns.countplot(x='is_rh_next', data=processed_data, palette='viridis') plt.title('用户转化分布 (0:未转化, 1:转化)') plt.xlabel('是否转化') plt.ylabel('用户数量') st.pyplot(fig) # 数值特征分布 st.markdown("**数值特征分布**") numeric_cols = ['AGE', 'ONLINE_DAY', 'TERM_CNT', 'PROM_AMT_MONTH'] # 动态计算子图布局 num_features = len(numeric_cols) if num_features > 0: ncols = 2 nrows = (num_features + ncols - 1) // ncols # 向上取整 fig, axes = plt.subplots(nrows, ncols, figsize=(14, 4*nrows)) # 将axes展平为一维数组 if nrows > 1 or ncols > 1: axes = axes.flatten() else: axes = [axes] # 单个子图时确保axes是列表 for i, col in enumerate(numeric_cols): if col in processed_data.columns and i < len(axes): sns.histplot(processed_data[col], kde=True, ax=axes[i], color='skyblue') axes[i].set_title(f'{col}分布') axes[i].set_xlabel('') # 隐藏多余的子图 for j in range(i+1, len(axes)): axes[j].set_visible(False) plt.tight_layout() st.pyplot(fig) else: st.warning("没有可用的数值特征") # 特征相关性分析 st.markdown("**特征相关性热力图**") corr_cols = numeric_cols + ['is_rh_next'] if len(corr_cols) > 1: corr_data = processed_data[corr_cols].corr() fig, ax = plt.subplots(figsize=(12, 8)) sns.heatmap(corr_data, annot=True, fmt=".2f", cmap='coolwarm', ax=ax) plt.title('特征相关性热力图') st.pyplot(fig) else: st.warning("特征不足,无法生成相关性热力图") # 模型训练 st.subheader("模型训练") # 训练参数设置 col1, col2 = st.columns(2) test_size = col1.slider("测试集比例", 0.1, 0.4, 0.2, 0.05) random_state = col2.number_input("随机种子", 0, 100, 42) # 开始训练按钮 if st.button("开始训练模型", use_container_width=True): with st.spinner("模型训练中,请稍候..."): # 创建Spark会话 spark = create_spark_session() # 将Pandas DataFrame转换为Spark DataFrame spark_df = spark.createDataFrame(processed_data) # 划分训练集和测试集 train_df, test_df = spark_df.randomSplit([1.0 - test_size, test_size], seed=random_state) # 特征工程 # 分类特征编码 categorical_cols = ['GENDER', 'MKT_STAR_GRADE_NAME', 'IF_YHTS'] # 只处理存在的分类特征 existing_cat_cols = [col for col in categorical_cols if col in processed_data.columns] indexers = [StringIndexer(inputCol=col, outputCol=col+"_index") for col in existing_cat_cols] encoders = [OneHotEncoder(inputCol=col+"_index", outputCol=col+"_encoded") for col in existing_cat_cols] # 数值特征 numeric_cols = ['AGE', 'ONLINE_DAY', 'TERM_CNT', 'PROM_AMT_MONTH'] # 组合所有特征 feature_cols = numeric_cols + [col+"_encoded" for col in existing_cat_cols] assembler = VectorAssembler(inputCols=feature_cols, outputCol="features") # 目标变量索引 label_indexer = StringIndexer(inputCol="is_rh_next", outputCol="label") # 构建模型 lr = LogisticRegression(featuresCol="features", labelCol="label") dt = DecisionTreeClassifier(featuresCol="features", labelCol="label") rf = RandomForestClassifier(featuresCol="features", labelCol="label") # 创建管道 pipeline_lr = Pipeline(stages=indexers + encoders + [assembler, label_indexer, lr]) pipeline_dt = Pipeline(stages=indexers + encoders + [assembler, label_indexer, dt]) pipeline_rf = Pipeline(stages=indexers + encoders + [assembler, label_indexer, rf]) # 训练模型 model_lr = pipeline_lr.fit(train_df) model_dt = pipeline_dt.fit(train_df) model_rf = pipeline_rf.fit(train_df) # 评估模型 evaluator_auc = BinaryClassificationEvaluator(labelCol="label", rawPredictionCol="rawPrediction") evaluator_acc = MulticlassClassificationEvaluator(labelCol="label", predictionCol="prediction", metricName="accuracy") evaluator_f1 = MulticlassClassificationEvaluator(labelCol="label", predictionCol="prediction", metricName="f1") def evaluate_model(model, data): predictions = model.transform(data) auc = evaluator_auc.evaluate(predictions) acc = evaluator_acc.evaluate(predictions) f1 = evaluator_f1.evaluate(predictions) return {"AUC": auc, "Accuracy": acc, "F1": f1} results = { "Logistic Regression": evaluate_model(model_lr, test_df), "Decision Tree": evaluate_model(model_dt, test_df), "Random Forest": evaluate_model(model_rf, test_df) } # 保存结果 st.session_state.model_results = results st.session_state.best_model = model_rf # 默认使用随机森林作为最佳模型 st.session_state.spark = spark st.success("🎉 模型训练完成!") # 显示模型性能 st.subheader("模型性能评估") # 转换为DataFrame展示 results_df = pd.DataFrame(results).T st.dataframe(results_df.style.format("{:.4f}").background_gradient(cmap='Blues')) # 可视化比较 fig, ax = plt.subplots(figsize=(10, 6)) results_df.plot(kind='bar', ax=ax) plt.title('模型性能比较') plt.ylabel('分数') plt.xticks(rotation=15) plt.legend(loc='upper right') st.pyplot(fig) # 特征重要性(随机森林) st.subheader("随机森林特征重要性") rf_model = model_rf.stages[-1] feature_importances = rf_model.featureImportances.toArray() feature_names = numeric_cols + [f"{col}_encoded" for col in existing_cat_cols] importance_df = pd.DataFrame({ "Feature": feature_names, "Importance": feature_importances }).sort_values("Importance", ascending=False).head(10) fig, ax = plt.subplots(figsize=(10, 6)) sns.barplot(x="Importance", y="Feature", data=importance_df, palette="viridis", ax=ax) plt.title('Top 10 重要特征') st.pyplot(fig) # 保存模型 model_path = "best_model" model_rf.write().overwrite().save(model_path) st.session_state.model_path = model_path except Exception as e: st.error(f"数据处理错误: {str(e)}") # 预测分析部分 else: st.markdown(""" <div class="card"> <h3>用户转化预测</h3> <p>预测单宽带用户转化为融合套餐的可能性</p> </div> """, unsafe_allow_html=True) # 上传预测数据 predict_file = st.file_uploader("上传预测数据 (CSV格式, GBK编码)", type=["csv"]) if predict_file is not None: try: # 读取数据 predict_data = pd.read_csv(predict_file, encoding='GBK') # 显示数据预览 with st.expander("数据预览", expanded=True): st.dataframe(predict_data.head()) # 检查是否有模型 if "model_path" not in st.session_state or not os.path.exists(st.session_state.model_path): st.warning("⚠️ 未找到训练好的模型,请先训练模型") st.stop() # 开始预测按钮 if st.button("开始预测", use_container_width=True): with st.spinner("预测进行中,请稍候..."): # 数据预处理 processed_data = preprocess_data(predict_data) # 创建Spark会话 if "spark" not in st.session_state: spark = create_spark_session() st.session_state.spark = spark else: spark = st.session_state.spark # 将Pandas DataFrame转换为Spark DataFrame spark_df = spark.createDataFrame(processed_data) # 加载模型 best_model = st.session_state.best_model # 生成预测结果 predictions = best_model.transform(spark_df) # 提取预测结果 predictions_df = predictions.select( "CCUST_ROW_ID", "probability", "prediction" ).toPandas() # 解析概率值 predictions_df['转化概率'] = predictions_df['probability'].apply(lambda x: float(x[1])) predictions_df['预测结果'] = predictions_df['prediction'].apply(lambda x: "可能转化" if x == 1.0 else "可能不转化") # 添加转化可能性等级 predictions_df['转化可能性'] = pd.cut( predictions_df['转化概率'], bins=[0, 0.3, 0.7, 1], labels=["低可能性", "中可能性", "高可能性"] ) # 保存结果 st.session_state.prediction_results = predictions_df st.success("✅ 预测完成!") except Exception as e: st.error(f"预测错误: {str(e)}") # 显示预测结果 if "prediction_results" in st.session_state: st.markdown(""" <div class="card"> <h3>预测结果</h3> <p>用户转化可能性评估报告</p> </div> """, unsafe_allow_html=True) result_df = st.session_state.prediction_results # 转化可能性分布 st.subheader("转化可能性分布概览") col1, col2, col3 = st.columns(3) high_conv = (result_df["转化可能性"] == "高可能性").sum() med_conv = (result_df["转化可能性"] == "中可能性").sum() low_conv = (result_df["转化可能性"] == "低可能性").sum() col1.markdown(f""" <div class="metric-card"> <div class="metric-value">{high_conv}</div> <div class="metric-label">高可能性用户</div> </div> """, unsafe_allow_html=True) col2.markdown(f""" <div class="metric-card"> <div class="metric-value">{med_conv}</div> <div class="metric-label">中可能性用户</div> </div> """, unsafe_allow_html=True) col3.markdown(f""" <div class="metric-card"> <div class="metric-value">{low_conv}</div> <div class="metric-label">低可能性用户</div> </div> """, unsafe_allow_html=True) # 转化可能性分布图 fig, ax = plt.subplots(figsize=(8, 5)) conv_counts = result_df["转化可能性"].value_counts() conv_counts.plot(kind='bar', color=['#4CAF50', '#FFC107', '#F44336'], ax=ax) plt.title('用户转化可能性分布') plt.xlabel('可能性等级') plt.ylabel('用户数量') st.pyplot(fig) # 详细预测结果 st.subheader("详细预测结果") # 样式函数 def color_convert(val): if val == "高可能性": return "background-color: #c8e6c9; color: #388e3c;" elif val == "中可能性": return "background-color: #fff9c4; color: #f57f17;" else: return "background-color: #ffcdd2; color: #c62828;" # 格式化显示 display_df = result_df[["CCUST_ROW_ID", "转化概率", "预测结果", "转化可能性"]] styled_df = display_df.style.format({ "转化概率": "{:.2%}" }).applymap(color_convert, subset=["转化可能性"]) st.dataframe(styled_df, height=400) # 下载结果 csv = display_df.to_csv(index=False).encode("utf-8") st.download_button( label="下载预测结果", data=csv, file_name="用户转化预测结果.csv", mime="text/csv", use_container_width=True ) 页脚 st.markdown(“—”) st.markdown(“”" <div style="text-align: center; color: #5c6bc0; font-size: 0.9rem; padding: 1rem;"> © 2023 精准营销系统 | 基于Spark和Streamlit开发 </div> """, unsafe_allow_html=True) 将上述所给代码,不使用spark,仿照如下所给代码,完成算法和模型调优等操作 import streamlit as st import pandas as pd import numpy as np import joblib import os import time import pandas as pd import numpy as np import matplotlib.pyplot as plt import matplotlib as mpl import matplotlib.font_manager as fm import seaborn as sns from sklearn.model_selection import train_test_split from sklearn.ensemble import RandomForestClassifier from sklearn.metrics import accuracy_score, roc_auc_score, confusion_matrix from sklearn.preprocessing import StandardScaler from imblearn.over_sampling import SMOTE from sklearn.impute import SimpleImputer import warnings warnings.filterwarnings("ignore") plt.rcParams[‘font.sans-serif’] = [‘SimHei’] plt.rcParams[‘axes.unicode_minus’] = False # 正确显示负号 页面设置 st.set_page_config( page_title=“风控违约预测系统”, page_icon=“📊”, layout=“wide”, initial_sidebar_state=“expanded” ) 自定义CSS样式 st.markdown(“”" <style> .stApp { background: linear-gradient(135deg, #f5f7fa 0%, #e4edf5 100%); font-family: 'Helvetica Neue', Arial, sans-serif; } .header { background: linear-gradient(90deg, #2c3e50 0%, #4a6491 100%); color: white; padding: 1.5rem; border-radius: 0.75rem; box-shadow: 0 4px 12px rgba(0,0,0,0.1); margin-bottom: 2rem; } .card { background: white; border-radius: 0.75rem; padding: 1.5rem; margin-bottom: 1.5rem; box-shadow: 0 4px 12px rgba(0,0,0,0.08); transition: transform 0.3s ease; } .card:hover { transform: translateY(-5px); box-shadow: 0 6px 16px rgba(0,0,0,0.12); } .stButton button { background: linear-gradient(90deg, #3498db 0%, #1a5276 100%) !important; color: white !important; border: none !important; border-radius: 0.5rem; padding: 0.75rem 1.5rem; font-size: 1rem; font-weight: 600; transition: all 0.3s ease; width: 100%; } .stButton button:hover { transform: scale(1.05); box-shadow: 0 4px 8px rgba(52, 152, 219, 0.4); } .feature-box { background: linear-gradient(135deg, #e3f2fd 0%, #bbdefb 100%); border-radius: 0.75rem; padding: 1.5rem; margin-bottom: 1.5rem; } .result-box { background: linear-gradient(135deg, #e8f5e9 0%, #c8e6c9 100%); border-radius: 0.75rem; padding: 1.5rem; margin-top: 1.5rem; } .model-box { background: linear-gradient(135deg, #fff3e0 0%, #ffe0b2 100%); border-radius: 0.75rem; padding: 1.5rem; margin-top: 1.5rem; } .stProgress > div > div > div { background: linear-gradient(90deg, #2ecc71 0%, #27ae60 100%) !important; } .metric-card { background: white; border-radius: 0.75rem; padding: 1rem; text-align: center; box-shadow: 0 4px 8px rgba(0,0,0,0.06); } .metric-value { font-size: 1.8rem; font-weight: 700; color: #2c3e50; } .metric-label { font-size: 0.9rem; color: #7f8c8d; margin-top: 0.5rem; } .highlight { background: linear-gradient(90deg, #ffeb3b 0%, #fbc02d 100%); padding: 0.2rem 0.5rem; border-radius: 0.25rem; font-weight: 600; } .stDataFrame { border-radius: 0.75rem; box-shadow: 0 4px 8px rgba(0,0,0,0.06); } .risk-high { background-color: #ffcdd2 !important; color: #c62828 !important; font-weight: 700; } .risk-medium { background-color: #fff9c4 !important; color: #f57f17 !important; font-weight: 600; } .risk-low { background-color: #c8e6c9 !important; color: #388e3c !important; } </style> “”", unsafe_allow_html=True) def preprocess_loan_data(data_old): “”" 训练时数据预处理函数,返回处理后的数据和推理时需要的参数 参数: data_old: 原始训练数据 (DataFrame) 返回: processed_data: 预处理后的训练数据 (DataFrame) preprocessor_params: 推理时需要的预处理参数 (dict) """ # 1. 创建原始数据副本 loan_data = data_old.copy() # 2. 保存要删除的列列表 drop_list = ['id','member_id', 'term', 'pymnt_plan', 'initial_list_status', 'sub_grade', 'emp_title', 'issue_d', 'title', 'zip_code', 'addr_state', 'earliest_cr_line', 'last_pymnt_d', 'last_credit_pull_d', 'url','desc','next_pymnt_d'] loan_data.drop([col for col in drop_list if col in loan_data.columns], axis=1, inplace=True, errors='ignore') # 3. 删除缺失值超过90%的列 #todo 自己补齐删除代码 missing_ratio = loan_data.isnull().sum() / len(loan_data) loan_data.drop(missing_ratio[missing_ratio > 0.9].index, axis=1, inplace=True, errors='ignore') # 4. 删除值全部相同的列 #todo 自己补齐删除代码 constant_cols = loan_data.columns[loan_data.nunique() <= 1] loan_data.drop(constant_cols, axis=1, inplace=True, errors='ignore') # 5. 处理特殊数值列 loans = loan_data # 修正变量名 loans["int_rate"] = loans["int_rate"].astype(str).str.rstrip('%').astype("float") loans["revol_util"] = loans["revol_util"].astype(str).str.rstrip('%').astype("float") # 6. 缺失值处理 ## 识别分类列和数值列 objectColumns = loans.select_dtypes(include=["object"]).columns.tolist() numColumns = loans.select_dtypes(include=[np.number]).columns.tolist() ## 保存分类列的列名 categorical_columns = objectColumns.copy() ## 填充分类变量缺失值 loans[objectColumns] = loans[objectColumns].fillna("Unknown") ## 填充数值变量缺失值并保存均值 imr = SimpleImputer(missing_values=np.nan, strategy="mean") loans[numColumns] = imr.fit_transform(loans[numColumns]) # 保存数值列的均值 numerical_means = {col: imr.statistics_[i] for i, col in enumerate(numColumns)} # 8. 特征衍生 loans["installment_feat"] = loans["installment"] / ((loans["annual_inc"] + 1) / 12) # 9. 目标变量编码 status_mapping = { "Current": 0, "Issued": 0, "Fully Paid": 0, "In Grace Period": 1, "Late (31-120 days)": 1, "Late (16-30 days)": 1, "Charged Off": 1, "Does not meet the credit policy. Status:Charged Off": 1, "Does not meet the credit policy. Status:Fully Paid": 0, "Default": 0 } loans["loan_status"] = loans["loan_status"].map(status_mapping) # 10. 有序特征映射 mapping_dict = { "emp_length": { "10+ years": 10, "9 years": 9, "8 years": 8, "7 years": 7, "6 years": 6, "5 years": 5, "4 years": 4, "3 years": 3, "2 years": 2, "1 year": 1, "< 1 year": 0, "Unknown": 0 }, "grade": { "A": 1, "B": 2, "C": 3, "D": 4, "E": 5, "F": 6, "G": 7 } } loans = loans.replace(mapping_dict) # 11. One-hot编码 n_columns = ["home_ownership", "verification_status", "purpose", "application_type"] dummy_df = pd.get_dummies(loans[n_columns], drop_first=False) loans = pd.concat([loans, dummy_df], axis=1) loans.drop(n_columns, axis=1, inplace=True) # 保存One-hot编码后的列名 onehot_columns = n_columns onehot_encoder_columns = dummy_df.columns.tolist() # 12. 特征缩放 # 识别需要缩放的数值列 numeric_cols = loans.select_dtypes(include=["int", "float"]).columns.tolist() if 'loan_status' in numeric_cols: numeric_cols.remove('loan_status') # 创建并拟合缩放器 sc = StandardScaler() if numeric_cols: loans[numeric_cols] = sc.fit_transform(loans[numeric_cols]) # 保存缩放列名 scaled_columns = numeric_cols # 13. 保存最终列结构(在SMOTE之前) #final_columns = loans.columns.tolist().remove('loan_status') final_columns = loans.columns[loans.columns != 'loan_status'].tolist() # 14. 处理不平衡数据(SMOTE过采样) X = loans.drop("loan_status", axis=1) y = loans["loan_status"] os = SMOTE(random_state=42) X_res, y_res = os.fit_resample(X, y) # 15. 合并为最终DataFrame processed_data = pd.concat([X_res, y_res], axis=1) processed_data.columns = list(X.columns) + ["loan_status"] # 16. 创建推理时需要的参数字典 preprocessor_params = { # 1. 删除的列 'drop_list': drop_list, # 2. 分类列缺失值填充 'categorical_columns': categorical_columns, # 3. 数值列填充均值 'numerical_means': numerical_means, # 4. 有序特征映射 'mapping_dict': mapping_dict, # 5. One-hot配置 'onehot_columns': onehot_columns, 'onehot_encoder_columns': onehot_encoder_columns, # 6. 缩放器及缩放列 'scaler': sc, # 已拟合的StandardScaler实例 'scaled_columns': scaled_columns, # 7. 最终列结构(训练后的列顺序) 'final_columns': final_columns } return processed_data, preprocessor_params def preprocess_loan_data_inference(data_old, preprocessor_params): “”" 推理时数据处理函数 参数: data_old: 原始推理数据 (DataFrame) preprocessor_params: 从训练过程保存的预处理参数 (dict) 返回: processed_data: 预处理后的推理数据 (DataFrame) """ # 1. 复制数据避免污染原始数据 loanss = data_old.copy() # 2. 删除训练时确定的列 drop_list = preprocessor_params['drop_list'] loans = loanss.drop(columns=[col for col in drop_list if col in loanss.columns], axis=1, errors='ignore') # 3. 处理特殊数值列(百分比转换) if 'int_rate' in loans: loans["int_rate"] = loans["int_rate"].astype(str).str.rstrip('%').astype("float") if 'revol_util' in loans: loans["revol_util"] = loans["revol_util"].astype(str).str.rstrip('%').astype("float") # 4. 特征衍生(使用训练时相同公式) if 'installment' in loans and 'annual_inc' in loans: loans["installment_feat"] = loans["installment"] / ((loans["annual_inc"] + 1) / 12) # 5. 有序特征映射(使用训练时的映射字典) mapping_dict = preprocessor_params['mapping_dict'] for col, mapping in mapping_dict.items(): if col in loans: # 处理未知值,默认为0 loans[col] = loans[col].map(mapping).fillna(0).astype(int) # 6. 缺失值处理(使用训练时保存的策略) # 分类变量 cat_cols = preprocessor_params['categorical_columns'] for col in cat_cols: if col in loans: loans[col] = loans[col].fillna("Unknown") # 数值变量(使用训练时保存的均值) num_means = preprocessor_params['numerical_means'] for col, mean_value in num_means.items(): if col in loans: loans[col] = loans[col].fillna(mean_value) # 7. One-hot编码(对齐训练时的列结构) n_columns = preprocessor_params['onehot_columns'] expected_dummy_columns = preprocessor_params['onehot_encoder_columns'] # 创建空DataFrame用于存储结果 dummy_df = pd.DataFrame(columns=expected_dummy_columns) # 为每个分类列生成dummy变量 for col in n_columns: if col in loans: # 为当前列生成dummies col_dummies = pd.get_dummies(loans[col], prefix=col) # 对齐训练时的列结构 for expected_col in expected_dummy_columns: if expected_col in col_dummies: dummy_df[expected_col] = col_dummies[expected_col] else: # 如果该列不存在,则创建全0列 dummy_df[expected_col] = 0 # 合并dummy变量 loans = pd.concat([loans, dummy_df], axis=1) # 删除原始分类列 loans.drop(columns=[col for col in n_columns if col in loans.columns], inplace=True, errors='ignore') # 8. 特征缩放(使用训练时的缩放器参数) sc = preprocessor_params['scaler'] scaled_cols = [col for col in preprocessor_params['scaled_columns'] if col in loans.columns] if scaled_cols: loans[scaled_cols] = sc.transform(loans[scaled_cols]) # 9. 对齐最终特征列(确保与训练数据相同) final_columns = preprocessor_params['final_columns'] # 添加缺失列(用0填充) for col in final_columns: if col not in loans.columns: loans[col] = 0 # 移除多余列并保持顺序 processed_data = loans[final_columns] print(loans.columns) return processed_data 标题区域 st.markdown(“”" <div class="header"> <h1 style='text-align: center; margin: 0;'>风控违约预测系统</h1> <p style='text-align: center; margin: 0.5rem 0 0; font-size: 1.1rem;'>基于机器学习的信贷风险评估与预测</p> </div> """, unsafe_allow_html=True) 页面布局 col1, col2 = st.columns([1, 1.5]) 左侧区域 - 图片和简介 with col1: st.markdown(“”" 智能风控系统 利用先进机器学习技术预测信贷违约风险 “”", unsafe_allow_html=True) # 使用在线图片作为占位符 st.image("https://images.unsplash.com/photo-1553877522-43269d4ea984?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=1200&q=80", caption="智能风控系统示意图", use_column_width=True) st.markdown(""" <div class="feature-box"> <h4>📈 系统功能</h4> <ul> <li>客户违约风险预测</li> <li>高精度机器学习模型</li> <li>可视化风险评估</li> <li>批量数据处理</li> </ul> </div> """, unsafe_allow_html=True) 右侧区域 - 功能选择 with col2: st.markdown(“”" 请选择操作类型 您可以选择训练新模型或使用现有模型进行预测 “”", unsafe_allow_html=True) # 功能选择 option = st.radio("", ["🚀 训练新模型 - 使用新数据训练预测模型", "🔍 推理预测 - 使用模型预测违约风险"], index=0, label_visibility="hidden") # 模型训练部分 if "训练新模型" in option: st.markdown(""" <div class="model-box"> <h4>模型训练</h4> <p>上传训练数据并训练新的预测模型</p> </div> """, unsafe_allow_html=True) # 上传训练数据 train_file = st.file_uploader("上传训练数据 (CSV格式)", type=["csv"]) if train_file is not None: try: # 读取数据 train_data_old = pd.read_csv(train_file) # 显示数据预览 with st.expander("数据预览", expanded=True): st.dataframe(train_data_old.head()) col1, col2, col3 = st.columns(3) col1.metric("总样本数", train_data_old.shape[0]) col2.metric("特征数量", train_data_old.shape[1] - 1) # 训练参数设置 st.subheader("训练参数") col1, col2 = st.columns(2) test_size = col1.slider("测试集比例", 0.1, 0.4, 0.2, 0.1) n_estimators = col2.slider("树的数量", 10, 500, 100, 10) max_depth = col1.slider("最大深度", 2, 30, 10, 1) random_state = col2.number_input("随机种子", 0, 100, 42) # 开始训练按钮 if st.button("开始训练模型", use_container_width=True): with st.spinner("模型训练中,请稍候..."): # 模拟数据处理 progress_bar = st.progress(0) train_data,preprocessor_params = preprocess_loan_data(train_data_old) joblib.dump(preprocessor_params, 'loan_preprocessor_params.pkl') # 步骤1: 数据预处理 time.sleep(1) progress_bar.progress(25) st.success("✅ 数据预处理完成") # 步骤2: 特征工程 time.sleep(1) progress_bar.progress(50) st.success("✅ 特征工程完成") # 步骤3: 模型训练 time.sleep(2) progress_bar.progress(75) # 实际训练代码 (简化版) X = train_data.drop("loan_status", axis=1) y = train_data["loan_status"] # 划分训练测试集 #todo 自己补齐数据划分代码 X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=test_size, random_state=random_state, stratify=y) # 训练模型 #todo 自己补齐调用随机森林算法完成模型的训练 model = RandomForestClassifier(n_estimators=n_estimators, max_depth=max_depth, random_state=random_state, n_jobs=-1) model.fit(X_train, y_train) # 保存模型 joblib.dump(model, "risk_model.pkl") # 步骤4: 模型评估 time.sleep(1) progress_bar.progress(100) # 评估模型 #todo 自己补齐调用预测函数完成测试集推理预测 y_pred = model.predict(X_test) y_proba = model.predict_proba(X_test)[:, 1] accuracy = accuracy_score(y_test, y_pred) auc = roc_auc_score(y_test, y_proba) # 保存评估结果 st.session_state.model_trained = True st.session_state.accuracy = accuracy st.session_state.auc = auc st.session_state.y_test = y_test st.session_state.y_pred = y_pred st.success("🎉 模型训练完成!") # 显示模型性能 st.subheader("模型性能评估") col1, col2 = st.columns(2) col1.markdown(f""" <div class="metric-card"> <div class="metric-value">{accuracy*100:.1f}%</div> <div class="metric-label">准确率</div> </div> """, unsafe_allow_html=True) col2.markdown(f""" <div class="metric-card"> <div class="metric-value">{auc:.3f}</div> <div class="metric-label">AUC 分数</div> </div> """, unsafe_allow_html=True) # 混淆矩阵 st.subheader("混淆矩阵") cm = confusion_matrix(y_test, y_pred) fig, ax = plt.subplots(figsize=(6, 4)) sns.heatmap(cm, annot=True, fmt="d", cmap="Blues", ax=ax) ax.set_xlabel("预测标签") ax.set_ylabel("真实标签") ax.set_title("混淆矩阵") st.pyplot(fig) # 特征重要性 st.subheader("特征重要性") feature_importance = pd.DataFrame({ "特征": X.columns, "重要性": model.feature_importances_ }).sort_values("重要性", ascending=False).head(10) fig, ax = plt.subplots(figsize=(10, 6)) sns.barplot(x="重要性", y="特征", data=feature_importance, palette="viridis", ax=ax) ax.set_title("Top 10 重要特征") st.pyplot(fig) except Exception as e: st.error(f"数据处理错误: {str(e)}") # 推理预测部分 else: st.markdown(""" <div class="model-box"> <h4>风险预测</h4> <p>上传需要预测的数据,生成违约风险评估报告</p> </div> """, unsafe_allow_html=True) # 上传预测数据 predict_file = st.file_uploader("上传预测数据 (CSV格式)", type=["csv"]) if predict_file is not None: try: # 读取数据 predict_data = pd.read_csv(predict_file) # 显示数据预览 with st.expander("数据预览", expanded=True): st.dataframe(predict_data.head()) st.info(f"数据集包含 {predict_data.shape[0]} 个样本,{predict_data.shape[1]} 个特征") # 检查是否有模型 if not os.path.exists("risk_model.pkl"): st.warning("⚠️ 未找到训练好的模型,请先训练模型或使用示例数据") # 使用示例模型 if st.button("使用示例模型进行预测", use_container_width=True): st.info("正在使用预训练的示例模型进行预测...") # 创建示例模型 X = np.random.rand(100, 10) y = np.random.randint(0, 2, 100) model = RandomForestClassifier(n_estimators=50, random_state=42) model.fit(X, y) # 生成预测结果 predictions = model.predict(predict_data.values) probas = model.predict_proba(predict_data.values)[:, 1] # 创建结果DataFrame result_df = pd.DataFrame({ "客户ID": predict_data["member_id"], "违约概率": probas, "预测标签": predictions }) # 添加风险等级 result_df["风险等级"] = pd.cut( result_df["违约概率"], bins=[0, 0.2, 0.5, 1], labels=["低风险", "中风险", "高风险"], include_lowest=True ) # 保存结果 st.session_state.prediction_results = result_df else: # 加载模型 model = joblib.load("risk_model.pkl") preprocessor_params = joblib.load('loan_preprocessor_params.pkl') # 开始预测按钮 if st.button("开始风险预测", use_container_width=True): with st.spinner("预测进行中,请稍候..."): # 模拟预测过程 progress_bar = st.progress(0) # 预处理推理数据 #todo 自己补齐调用推理数据处理函数完成推理数据的清洗 processed_inference = preprocess_loan_data_inference(predict_data, preprocessor_params) # 步骤1: 数据预处理 time.sleep(1) progress_bar.progress(25) # 步骤2: 特征工程 time.sleep(1) progress_bar.progress(50) # 步骤3: 模型预测 time.sleep(1) progress_bar.progress(75) # 生成预测结果 predictions = model.predict(processed_inference.values) probas = model.predict_proba(processed_inference.values)[:, 1] # 创建结果DataFrame result_df = pd.DataFrame({ "客户ID": predict_data["member_id"], "违约概率": probas, "预测标签": predictions }) # 添加风险等级 result_df["风险等级"] = pd.cut( result_df["违约概率"], bins=[0, 0.2, 0.5, 1], labels=["低风险", "中风险", "高风险"], include_lowest=True ) # 步骤4: 生成报告 time.sleep(1) progress_bar.progress(100) # 保存结果 st.session_state.prediction_results = result_df st.success("✅ 预测完成!") except Exception as e: st.error(f"预测错误: {str(e)}") # 显示预测结果 if "prediction_results" in st.session_state: st.markdown(""" <div class="result-box"> <h4>预测结果</h4> <p>客户违约风险评估报告</p> </div> """, unsafe_allow_html=True) result_df = st.session_state.prediction_results # 风险分布 st.subheader("风险分布概览") col1, col2, col3 = st.columns(3) high_risk = (result_df["风险等级"] == "高风险").sum() med_risk = (result_df["风险等级"] == "中风险").sum() low_risk = (result_df["风险等级"] == "低风险").sum() col1.markdown(f""" <div class="metric-card"> <div class="metric-value risk-high">{high_risk}</div> <div class="metric-label">高风险客户</div> </div> """, unsafe_allow_html=True) col2.markdown(f""" <div class="metric-card"> <div class="metric-value risk-medium">{med_risk}</div> <div class="metric-label">中风险客户</div> </div> """, unsafe_allow_html=True) col3.markdown(f""" <div class="metric-card"> <div class="metric-value risk-low">{low_risk}</div> <div class="metric-label">低风险客户</div> </div> """, unsafe_allow_html=True) # 风险分布图 fig, ax = plt.subplots(figsize=(8, 4)) risk_counts = result_df["风险等级"].value_counts() risk_counts.plot(kind="bar", color=["#4CAF50", "#FFC107", "#F44336"], ax=ax) ax.set_title("客户风险等级分布") ax.set_xlabel("风险等级") ax.set_ylabel("客户数量") st.pyplot(fig) # 详细预测结果 st.subheader("详细预测结果") # 样式函数 def color_risk(val): if val == "高风险": return "background-color: #ffcdd2; color: #c62828;" elif val == "中风险": return "background-color: #fff9c4; color: #f57f17;" else: return "background-color: #c8e6c9; color: #388e3c;" # 格式化显示 styled_df = result_df.style.applymap(color_risk, subset=["风险等级"]) st.dataframe(styled_df.format({ "违约概率": "{:.2%}" }), height=400) # 下载结果 csv = result_df.to_csv(index=False).encode("utf-8") st.download_button( label="下载预测结果", data=csv, file_name="风险预测结果.csv", mime="text/csv", use_container_width=True ) 页脚 st.markdown(“—”) st.markdown(“”" <div style="text-align: center; color: #7f8c8d; font-size: 0.9rem; padding: 1rem;"> © 2023 风控违约预测系统 | 基于Streamlit开发 </div> """, unsafe_allow_html=True)
07-03
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值