Netty In Action中文版 - 第十五章:选择正确的线程模型

本章详细介绍了Netty的线程模型,包括事件循环、并发、任务执行与调度等内容。通过对比Netty与其他网络框架的线程模型,突出Netty在设计上的优势。此外,阐述了如何利用事件循环执行I/O操作,以及如何在应用程序中调度任务执行。文章还介绍了Netty内部实现的细节,包括如何高效地调度任务,以及如何在不同版本中合理分配I/O线程。

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

http://blog.youkuaiyun.com/abc_key/article/details/38419469

本章介绍

  • 线程模型(thread-model)
  • 事件循环(EventLoop)
  • 并发(Concurrency)
  • 任务执行(task execution)
  • 任务调度(task scheduling)
        线程模型定义了应用程序或框架如何执行你的代码,选择应用程序/框架的正确的线程模型是很重要的。Netty提供了一个简单强大的线程模型来帮助我们简化代码,Netty对所有的核心代码都进行了同步。所有ChannelHandler,包括业务逻辑,都保证由一个线程同时执行特定的通道。这并不意味着Netty不能使用多线程,只是Netty限制每个连接都由一个线程处理,这种设计适用于非阻塞程序。我们没有必要去考虑多线程中的任何问题,也不用担心会抛ConcurrentModificationException或其他一些问题,如数据冗余、加锁等,这些问题在使用其他框架进行开发时是经常会发生的。

        读完本章就会深刻理解Netty的线程模型以及Netty团队为什么会选择这样的线程模型,这些信息可以让我们在使用Netty时让程序由最好的性能。此外,Netty提供的线程模型还可以让我们编写整洁简单的代码,以保持代码的整洁性;我们还会学习Netty团队的经验,过去使用其他的线程模型,现在我们将使用Netty提供的更容易更强大的线程模型来开发。

        尽管本章讲述的是Netty的线程模型,但是我们仍然可以使用其他的线程模型;至于如何选择一个完美的线程模型应该根据应用程序的实际需求来判断。

        本章假设如下:

  • 你明白线程是什么以及如何使用,并有使用线程的工作经验;若不是这样,就请花些时间来了解清楚这些知识。推荐一本书:Java并发编程实战。
  • 你了解多线程应用程序及其设计,也包括如何保证线程安全和获取最佳性能。
  • 你了解java.util.concurrent以及ExecutorService和ScheduledExecutorService。

15.1 线程模型概述

        本节将简单介绍一般的线程模型,Netty中如何使用指定的线程模型,以及Netty不同的版本中使用的线程模型。你会更好的理解不同的线程模型的所有利弊。
        如果思考一下,在我们的生活中会发现很多情况都会使用线程模型。例如,你有一个餐厅,向你的客户提供食品,食物需要在厨房煮熟后才能给客户;某个客户下了订单后,你需要将煮熟事物这个任务发送到厨房,而厨房可以以不同的方式来处理,这就像一个线程模型,定义了如何执行任务。
  • 只有一个厨师:
    • 这种方法是单线程的,一次只执行一个任务,完成当前订单后再处理下一个。
  • 你有多个厨师,每个厨师都可以做,空闲的厨师准备着接单做饭:
    • 这种方式是多线程的,任务由多个线程(厨师)执行,可以并行同时执行。
  • 你有多个厨师并分成组,一组做晚餐,一个做其他:
    • 这种情况也是多线程,但是带有额外的限制;同时执行多个任务是由实际执行的任务类型(晚餐或其他)决定。
        从上面的例子看出,日常活动适合在一个线程模型。但是Netty在这里适用吗?不幸的是,它没有那么简单,Netty的核心是多线程,但隐藏了来自用户的大部分。Netty使用多个线程来完成所有的工作,只有一个线程模型线型暴露给用户。大多数现代应用程序使用多个线程调度工作,让应用程序充分使用系统的资源来有效工作。在早期的Java中,这样做是通过按需创建新线程并行工作。但很快发现者不是完美的方案,因为创建和回收线程需要较大的开销。在Java5中加入了线程池,创建线程和重用线程交给一个任务执行,这样使创建和回收线程的开销降到最低。
        下图显示使用一个线程池执行一个任务,提交一个任务后会使用线程池中空闲的线程来执行,完成任务后释放线程并将线程重新放回线程池:

        上图每个任务线程的创建和回收不需要新线程去创建和销毁,但这只是一半的问题,我们稍后学习。你可能会问为什么不使用多线程,使用一个ExecutorService可以有助于防止线程创建和回收的成本?
        使用多线程会有太多的上下文切换,提高了资源和管理成本,这种副作用会随着运行线程的数量和执行的任务数量的增加而愈加明显。使用多线程在刚开始可能没有什么问题,但随着系统的负载增加,可能在某个点就会让系统崩溃。
        除了这些技术上的限制和问题,在项目生命周期内维护应用程序/框架可能还会发生其他问题。它有效的说明了增加应用程序的复杂性取决于它是平行的,简单的陈述:编写多线程应用程序时一个辛苦的工作!我们怎么来解决这个问题呢?在实际的场景中需要多个线程模型。让我们来看看Netty是如何解决这个问题的。

15.2 事件循环

        事件循环所做的正如它的名字,它运行的事件在一个循环中,直到循环终止。这非常适合网络框架的设计,因为它们需要为一个特定的连接运行一个事件循环。这不是Netty的新发明,其他的框架和实现已经很早就这样做了。
        在Netty中使用EventLoop接口代表事件循环,EventLoop是从EventExecutor和ScheduledExecutorService扩展而来,所以可以讲任务直接交给EventLoop执行。类关系图如下:

15.2.1 使用事件循环

        下面代码显示如何访问已分配给通道的EventLoop并在EventLoop中执行任务:
		Channel ch = ...;
		ch.eventLoop().execute(new Runnable() {
			@Override
			public void run() {
				System.out.println("run in the eventloop");
			}
		});
        使用事件循环的好处是不需要担心同步问题,在同一线程中执行所有其他关联通道的其他事件。这完全符合Netty的线程模型。检查任务是否已执行,使用返回的Future,使用Future可以访问很多不同的操作。下面的代码是检查任务是否执行:
		Channel ch = ...;
		Future<?> future = ch.eventLoop().submit(new Runnable() {
			@Override
			public void run() {
				
			}
		});
		if(future.isDone()){
			System.out.println("task complete");
		}else {
			System.out.println("task not complete");
		}
        检查执行任务是否在事件循环中:
		Channel ch = ...;
		if(ch.eventLoop().inEventLoop()){
			System.out.println("in the EventLoop");
		}else {
			System.out.println("outside the EventLoop");
		}
        只有确认没有其他EventLoop使用线程池了才能关闭线程池,否则可能会产生未定义的副作用。

15.2.2 Netty4中的I/O操作

        这个实现很强大,甚至Netty使用它来处理底层I/O事件,在socket上触发读和写操作。这些读和写操作是网络API的一部分,通过java和底层操作系统提供。下图显示在EventLoop上下文中执行入站和出站操作,如果执行线程绑定到EventLoop,操作会直接执行;如果不是,该线程将排队执行:

        需要一次处理一个事件取决于事件的性质,通常从网络堆栈读取或传输数据到你的应用程序,有时在另外的方向做同样的事情,例如从你的应用程序传输数据到网络堆栈再发送到远程对等通道,但不限于这种类型的事物;更重要的是使用的逻辑是通用的,灵活处理各种各样的案例。
        应该指出的是,线程模型(事件循环的顶部)描述并不总是由Netty使用。我们在了解Netty3后会更容易理解为什么新的线程模型是可取的。

15.2.3 Netty3中的I/O操作

        在以前的版本有点不同,Netty保证在I/O线程中只有入站事件才被执行,所有的出站时间被调用线程处理。这看起来是个好方案,但很容易出错。它还将负责同步ChannelHandler来处理这些事件,因为它不保证只有一个线程同时操作;这可能发生在你去掉通道下游事件的同时,例如,在不同的线程调用Channel.write(...)。下图显示Netty3的执行流程:

        除了需要负担同步ChannelHandler,这个线程模型的另一个问题是你可能需要去掉一个入站事件作为一个出站事件的结果,例如Channel.write(...)操作导致异常。在这种情况下,捕获的异常必须生成并抛出去。乍看之下这不像是一个问题,但我们知道,捕获异常由入站事件涉及,会让你知道问题出在哪里。问题是,事实上,你现在的情况是在调用线程上执行,但捕获到异常事件必须交给工作线程来执行。这是可行的,但如果你忘了传递过去,它会导致线程模型失效;假设入站事件只有一个线程不是真,这可能会给你各种各样的竞争条件。
        以前的实现有一个唯一的积极影响,在某些情况下它可以提供更好的延迟;成本是值得的,因为它消除了复杂性。实际上,在大多数应用程序中,你不会遵守任何差异延迟,还取决于其他因数,如:
  • 字节写入到远程对等通道有多快
  • I/O线程是否繁忙
  • 上下文切换
  • 锁定
你可以看到很多细节影响整体延迟。

15.2.4 Netty线程模型内部

        Netty的内部实现使其线程模型表现优异,它会检查正在执行的线程是否是已分配给实际通道(和EventLoop),在Channel的生命周期内,EventLoop负责处理所有的事件。如果线程是相同的EventLoop中的一个,讨论的代码块被执行;如果线程不同,它安排一个任务并在一个内部队列后执行。通常是通过EventLoop的Channel只执行一次下一个事件,这允许直接从任何线程与通道交互,同时还确保所有的ChannelHandler是线程安全,不需要担心并发访问问题。
        下图显示在EventLoop中调度任务执行逻辑,这适合Netty的线程模型:

        设计是非常重要的,以确保不要把任何长时间运行的任务放在执行队列中,因为长时间运行的任务会阻止其他在相同线程上执行的任务。这多少会影响整个系统依赖于EventLoop实现用于特殊传输的实现。传输之间的切换在你的代码库中可能没有任何改变,重要的是:切勿阻塞I/O线程。如果你必须做阻塞调用(或执行需要长时间才能完成的任务),使用EventExecutor。
        下一节将讲解一个在应用程序中经常使用的功能,就是调度执行任务(定期执行)。Java对这个需求提供了解决方案,但Netty提供了几个更好的方案。

15.3 调度任务执行

        每隔一段时间需要调度任务执行,也许你想注册一个任务在客户端完成连接5分钟后执行,一个常见的用例是发送一个消息“你还活着?”到远程对等通道,如果远程对等通道没有反应,则可以关闭通道(连接)和释放资源。就像你和朋友打电话,沉默了一段时间后,你会说“你还在吗?”,如果朋友没有回复,就可能是断线或朋友睡着了;不管是什么问题,你都可以挂断电话,没有什么可等待的;你挂了电话后,收起电话可以做其他的事。
        本节介绍使用强大的EventLoop实现任务调度,还会简单介绍Java API的任务调度,以方便和Netty比较加深理解。

15.3.1 使用普通的Java API调度任务

        在Java中使用JDK提供的ScheduledExecutorService实现任务调度。使用Executors提供的静态方法创建ScheduledExecutorService,有如下方法:
  • newScheduledThreadPool(int)
  • newScheduledThreadPool(int, ThreadFactory)
  • newSingleThreadScheduledExecutor()
  • newSingleThreadScheduledExecutor(ThreadFactory)
        看下面代码:
		ScheduledExecutorService executor = Executors.newScheduledThreadPool(10);
		ScheduledFuture<?> future = executor.schedule(new Runnable() {
			@Override
			public void run() {
				System.out.println("now it is 60 seconds later");
			}
		}, 60, TimeUnit.SECONDS);
		if(future.isDone()){
			System.out.println("scheduled completed");
		}
		//.....
		executor.shutdown();

15.3.2 使用EventLoop调度任务

        使用ScheduledExecutorService工作的很好,但是有局限性,比如在一个额外的线程中执行任务。如果需要执行很多任务,资源使用就会很严重;对于像Netty这样的高性能的网络框架来说,严重的资源使用是不能接受的。Netty对这个问题提供了很好的方法。
        Netty允许使用EventLoop调度任务分配到通道,如下面代码:
		Channel ch = ...;
		ch.eventLoop().schedule(new Runnable() {
			@Override
			public void run() {
				System.out.println("now it is 60 seconds later");
			}
		}, 60, TimeUnit.SECONDS);
        如果想任务每隔多少秒执行一次,看下面代码:
		Channel ch = ...;
		ScheduledFuture<?> future = ch.eventLoop().scheduleAtFixedRate(new Runnable() {
			@Override
			public void run() {
				System.out.println("after run 60 seconds,and run every 60 seconds");
			}
		}, 60, 60, TimeUnit.SECONDS);
		// cancel the task
		future.cancel(false);

15.3.3 调度的内部实现

        Netty内部实现其实是基于George Varghese提出的“Hashed  and  hierarchical  timing wheels: Data structures  to efficiently implement timer facility(散列和分层定时轮:数据结构有效实现定时器)”。这种实现只保证一个近似执行,也就是说任务的执行可能不是100%准确;在实践中,这已经被证明是一个可容忍的限制,不影响多数应用程序。所以,定时执行任务不可能100%准确的按时执行。
        为了更好的理解它是如何工作,我们可以这样认为:
  1. 在指定的延迟时间后调度任务;
  2. 任务被插入到EventLoop的Schedule-Task-Queue(调度任务队列);
  3. 如果任务需要马上执行,EventLoop检查每个运行;
  4. 如果有一个任务要执行,EventLoop将立刻执行它,并从队列中删除;
  5. EventLoop等待下一次运行,从第4步开始一遍又一遍的重复。
        因为这样的实现计划执行不可能100%正确,对于多数用例不可能100%准备的执行计划任务;在Netty中,这样的工作几乎没有资源开销。但是如果需要更准确的执行呢?很容易,你需要使用ScheduledExecutorService的另一个实现,这不是Netty的内容。记住,如果不遵循Netty的线程模型协议,你将需要自己同步并发访问。

15.4 I/O线程分配细节

        Netty使用线程池来为Channel的I/O和事件服务,不同的传输实现使用不同的线程分配方式;异步实现是只有几个线程给通道之间共享,这样可以使用最小的线程数为很多的平道服务,不需要为每个通道都分配一个专门的线程。
        下图显示如何分配线程池:

        如上图所示,使用一个固定大小的线程池管理三个线程,创建线程池后就把线程分配给线程池,确保在需要的时候,线程池中有可用的线程。这三个线程会分配给每个新创建的已连接通道,这是通过EventLoopGroup实现的,使用线程池来管理资源;实际会平均分配通道到所有的线程上,这种分布以循环的方式完成,因此它可能不会100%准确,但大部分时间是准确的。
        一个通道分配到一个线程后,在这个通道的生命周期内都会一直使用这个线程。这一点在以后的版本中可能会被改变,所以我们不应该依赖这种方式;不会被改变的是一个线程在同一时间只会处理一个通道的I/O操作,我们可以依赖这种方式,因为这种方式可以确保不需要担心同步。
        下图显示OIO(Old Blocking I/O)传输:

        从上图可以看出,每个通道都有一个单独的线程。我们可以使用java.io.*包里的类来开发基于阻塞I/O的应用程序,即使语义改变了,但有一件事仍然保持不变,每个通道的I/O在同时只能被一个线程处理;这个线程是由Channel的EventLoop提供,我们可以依靠这个硬性的规则,这也是Netty框架比其他网络框架更容易编写的原因。

15.5 Summary

本章主要讲解Netty的线程模型,其核心接口是EventLoop;并和OIO中的线程模型做了比较,以突显Netty的优异性。

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 os import time import warnings import tempfile import subprocess import sys # 忽略警告 warnings.filterwarnings("ignore") # 检查Java版本 def check_java_version(): try: java_version = subprocess.check_output(['java', '-version'], stderr=subprocess.STDOUT, text=True) st.info(f"Java版本信息:\n{java_version}") if 'version "1.8' in java_version: st.success("检测到Java 8 (1.8.x),已启用兼容模式") st.warning("注意:Spark 3.0+ 官方推荐使用 Java 11,但我们将尝试兼容 Java 8") elif 'version "11' in java_version or 'version "17' in java_version: st.success("Java版本兼容") else: st.warning(f"检测到未知Java版本: {java_version}") except Exception as e: st.error(f"无法检查Java版本: {str(e)}") st.stop() # 设置中文字体 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; } .java-warning { background-color: #fff3cd; border-left: 4px solid #ffc107; padding: 1rem; margin-bottom: 1.5rem; border-radius: 0 0.25rem 0.25rem 0; } </style> """, unsafe_allow_html=True) # 创建Spark会话 - 兼容Java 8 def create_spark_session(): # 针对Java 8的兼容性配置 # 1. 使用更小的内存配置避免资源问题 # 2. 添加Java 8特定的兼容性配置 # 3. 使用更低的Spark版本兼容性设置 return SparkSession.builder \ .appName("TelecomPrecisionMarketing") \ .config("spark.driver.memory", "1g") \ .config("spark.executor.memory", "1g") \ .config("spark.sql.shuffle.partitions", "4") \ .config("spark.driver.extraJavaOptions", "-Dio.netty.tryReflectionSetAccessible=true -XX:+UseG1GC") \ .config("spark.executor.extraJavaOptions", "-Dio.netty.tryReflectionSetAccessible=true -XX:+UseG1GC") \ .config("spark.network.timeout", "800s") \ .config("spark.executor.heartbeatInterval", "60s") \ .config("spark.sql.legacy.allowUntypedScalaUDF", "true") \ .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: median_val = df[col].median() df[col].fillna(median_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] if not df[col].mode().empty else '未知' 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) # Java版本检查 check_java_version() # 页面布局 col1, col2 = st.columns([1, 1.5]) # 左侧区域 - 图片和简介 with col1: st.markdown(""" <div class="card"> <h2>📱 智能营销系统</h2> <p>预测单宽带用户转化为融合套餐用户的可能性</p> </div> """, 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="精准营销系统示意图", use_column_width=True) 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(""" <div class="card"> <h3>📋 请选择操作类型</h3> <p>您可以选择数据分析或使用模型进行预测</p> </div> """, unsafe_allow_html=True) # 功能选择 - 添加标签 option = st.radio("操作类型", ["📊 数据分析 - 探索数据并训练模型", "🔍 预测分析 - 预测用户转化可能性"], index=0) # 数据分析部分 if "数据分析" in option: st.markdown(""" <div class="card"> <h3>数据分析与模型训练</h3> <p>上传数据并训练预测模型</p> </div> """, 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 tempfile.TemporaryDirectory() as tmp_dir: model_path = os.path.join(tmp_dir, "best_model") progress_bar = st.progress(0) status_text = st.empty() # 步骤1: 创建Spark会话 status_text.text("步骤1/7: 初始化Spark会话...") spark = create_spark_session() progress_bar.progress(15) # 步骤2: 转换为Spark DataFrame status_text.text("步骤2/7: 转换数据为Spark格式...") spark_df = spark.createDataFrame(processed_data) progress_bar.progress(30) # 步骤3: 划分训练集和测试集 status_text.text("步骤3/7: 划分训练集和测试集...") train_df, test_df = spark_df.randomSplit([1.0 - test_size, test_size], seed=random_state) progress_bar.progress(40) # 步骤4: 特征工程 status_text.text("步骤4/7: 特征工程处理...") 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") progress_bar.progress(50) # 步骤5: 构建模型 status_text.text("步骤5/7: 构建和训练模型...") # 使用更简单的模型配置 - 针对Java 8优化 rf = RandomForestClassifier( featuresCol="features", labelCol="label", numTrees=30, # 减少树的数量以适应Java 8 maxDepth=4, # 限制深度 seed=random_state, maxBins=32 # 减少bin数量以提高兼容性 ) pipeline = Pipeline(stages=indexers + encoders + [assembler, label_indexer, rf]) model = pipeline.fit(train_df) progress_bar.progress(80) # 步骤6: 评估模型 status_text.text("步骤6/7: 评估模型性能...") predictions = model.transform(test_df) evaluator_auc = BinaryClassificationEvaluator(labelCol="label", rawPredictionCol="rawPrediction") evaluator_acc = MulticlassClassificationEvaluator(labelCol="label", predictionCol="prediction", metricName="accuracy") auc = evaluator_auc.evaluate(predictions) acc = evaluator_acc.evaluate(predictions) results = { "Random Forest": {"AUC": auc, "Accuracy": acc} } progress_bar.progress(95) # 步骤7: 保存结果 status_text.text("步骤7/7: 保存模型和结果...") model.write().overwrite().save(model_path) st.session_state.model_results = results st.session_state.best_model = model st.session_state.model_path = model_path st.session_state.spark = spark progress_bar.progress(100) st.success("🎉 模型训练完成!") # 显示模型性能 st.subheader("模型性能评估") results_df = pd.DataFrame(results).T st.dataframe(results_df.style.format("{:.4f}").background_gradient(cmap='Blues')) # 特征重要性 st.subheader("特征重要性") rf_model = model.stages[-1] feature_importances = rf_model.featureImportances.toArray() importance_df = pd.DataFrame({ "Feature": feature_cols, "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) except Exception as e: st.error(f"模型训练错误: {str(e)}") st.error("提示:Java 8兼容性问题可能导致此错误,请尝试升级到Java 11") # 预测分析部分 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: st.warning("⚠️ 未找到训练好的模型,请先训练模型") st.stop() # 开始预测按钮 if st.button("开始预测", use_container_width=True): progress_bar = st.progress(0) status_text = st.empty() # 步骤1: 数据预处理 status_text.text("步骤1/4: 数据预处理中...") processed_data = preprocess_data(predict_data) progress_bar.progress(25) # 步骤2: 创建Spark会话 status_text.text("步骤2/4: 初始化Spark会话...") if "spark" not in st.session_state: spark = create_spark_session() st.session_state.spark = spark else: spark = st.session_state.spark progress_bar.progress(50) # 步骤3: 预测 status_text.text("步骤3/4: 进行预测...") spark_df = spark.createDataFrame(processed_data) best_model = st.session_state.best_model predictions = best_model.transform(spark_df) progress_bar.progress(75) # 步骤4: 处理结果 status_text.text("步骤4/4: 处理预测结果...") 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 progress_bar.progress(100) st.success("✅ 预测完成!") except Exception as e: st.error(f"预测错误: {str(e)}") st.error("提示:Java 8兼容性问题可能导致此错误,请尝试升级到Java 11") # 显示预测结果 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开发 | Java 8兼容模式 </div> """, unsafe_allow_html=True) 执行上述代码,系统已成功配置java17,但是仍然在spark初始会话卡住,给出修改后完整代码
最新发布
07-02
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值