
1. 毕业设计选题逻辑为什么选就业数据分析与可视化这个方向1.1 选题背后的现实需求每年毕业季大学生就业都是绕不开的话题。我在确定毕业设计选题时最初列了几个方向包括图像识别、推荐系统、文本分类等最后选定大学生就业数据分析与可视化这个题目核心原因有两个。第一数据可得性和真实感。图像识别需要大量标注数据推荐系统需要用户行为日志这些在毕设阶段很难拿到高质量数据。而就业数据在各招聘平台、高校就业信息网都有公开披露爬虫采集的可行性很高。更关键的是就业数据本身离每个学生都很近——工资多少、哪些岗位需求大、哪些城市机会多这些问题每个人都能感知做出来的可视化看板有天然的说服力。第二技术栈完整度高。这个题目能同时覆盖爬虫采集、数据清洗、数据库设计、后端API开发、前端可视化、深度学习预测算法几乎横跨了大学四年学的核心课程答辩时有完整的技术链路可以讲不会出现只有一个模型或只有几张图表的偏科情况。1.2 技术栈选型为什么要用Django Vue 深度学习三层架构选型阶段我对比了三套方案纯Python脚本生成静态HTML、Flask 原生JS/jQuery、Django Vue前后端分离。最终选择了Django Vue 深度学习的三层架构对应项目标题中最核心的三个关键词。Django承担数据采集、数据处理、接口提供和算法落地的角色。它自带ORM能把爬下来的数据直接映射成数据库表省去大量手写SQL的功夫Django REST framework封装了序列化器返回JSON格式做前后端分离非常顺畅。对于毕设这个规模的项目Django的项目结构和admin后台简直就是开箱即用不用额外造轮子。Vue负责前端可视化的呈现。ECharts图表库在Vue里集成非常成熟直接npm安装即可使用。Vue的单文件组件组织方式把每个图表拆成独立组件看板页面的代码结构比传统jQuery时代清晰一个量级。深度学习算法用于就业趋势预测模块。我采用LSTM循环神经网络对历史薪资和招聘需求量做时序预测这个选择在后面的章节会详细展开。三层架构的分工很明确爬虫脚本 Django后端负责数据从哪里来MySQL数据库负责数据怎么存DRF接口负责数据怎么给Vue ECharts负责数据怎么展示LSTM模型负责数据怎么预测。1.3 项目功能模块总览在动手写代码之前我先花了三天时间把功能模块拆解清楚。这个环节非常关键建议所有做毕设的同学都提前做它决定了后面开发迭代的效率。最终确定的功能清单如下岗位数据采集模块从公开招聘网站爬取岗位信息包括岗位名称、薪资范围、学历要求、工作经验、城市、行业类别等字段数据清洗入库模块对爬取的原始数据做去重、字段解析、归一化处理最终持久化到MySQL数据库就业数据统计分析模块按行业、城市、学历、经验等级等维度统计岗位分布和薪资水平可视化看板模块以大屏看板形式呈现整体就业态势包括岗位需求趋势折线图、薪资分布直方图、城市热度地图、技能需求词云就业趋势预测模块基于历史岗位数据训练LSTM模型预测未来6个月的薪资变化趋势和岗位需求量后台管理模块利用Django自带的Admin系统做数据管理方便答辩时展示数据录入和查询能力模块拆清楚以后开发顺序就自然出来了先爬数据再清洗入库接着做统计接口再画前端图表最后训练模型接入预测模块。按这个顺序推进每个阶段都有可交付的中间成果不会出现到最后才慌慌张张联调的情况。2. 数据集构建从爬虫采集到ETL清洗的完整链路2.1 数据来源与爬虫采集策略数据是整个项目的底座数据质量直接决定可视化效果和模型预测精度。我的数据来源是某大型招聘网站的公开职位信息页面采集技术选型上用的是Python的requests BeautifulSoup组合没有上Scrapy框架。原因很简单毕设场景不需要分布式爬取和调度队列Scrapy的学习成本反而拖慢进度。爬虫部分最核心的是反爬策略。招聘网站有比较严格的反爬机制我实践中比较管用的三件套是import requests from bs4 import BeautifulSoup import random import time headers { User-Agent: random.choice([ Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36, Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36, Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 ]), Referer: https://www.zhipin.com/, Accept-Language: zh-CN,zh;q0.9 } def fetch_page(url): for retry in range(3): try: time.sleep(random.uniform(1.5, 3.5)) resp requests.get(url, headersheaders, timeout10) if resp.status_code 200: return resp.text elif resp.status_code 403: time.sleep(random.uniform(5, 10)) continue except requests.exceptions.RequestException as e: print(f请求异常: {e}) return None请求频率控制是反爬的关键。我自己实测下来每两次请求之间间隔1.5到3.5秒会比较稳太快很容易被识别为爬虫导致IP被封。爬虫程序跑的时候要记录日志方便定位是哪些URL失败以及失败的原因是超时、被反爬还是页面结构变化。采集的字段包括岗位名称、公司名称、薪资范围文本如15-20K、城市、学历要求、工作经验要求、行业类别、发布时间。此外我还加了一个采集时间字段用于分析岗位发布的时序特征。2.2 数据清洗规则设计原始数据质量参差不齐最常见的几个问题我在这里列出来这些都是做数据分析与可视化必须处理的基础环节薪资字段解析是我踩坑最多的一个点。网站上薪资的文本格式五花八门有15-20K、8千-1.2万、20-30K·14薪、面议等多种写法。我的清洗策略是先用正则匹配出数字部分再统一转换成月薪数值最后取区间中值作为统计分析用的代表值。import re def parse_salary(salary_str): if not salary_str or 面议 in salary_str: return None # 匹配薪资数字范围 nums re.findall(r(\d\.?\d*)\s*[Kk千w万], salary_str) if not nums: return None num_list [float(n) for n in nums] # 统一换算为千元单位 if 万 in salary_str: num_list [n * 10 for n in num_list] elif 千 in salary_str or k in salary_str.lower(): num_list num_list if len(num_list) 1: return num_list[0] return sum(num_list) / len(num_list)重复数据去重这块我采用公司名称 岗位名称 城市作为联合唯一键因为同一个岗位在同一家公司下通常只有一个职位条目即使发布时间不同也算重复。用Django的ORM做去重时直接用distinct()配合filter()比较灵活。岗位类别归一化是把五花八门的岗位名称归到几个大类别里。比如Java开发工程师、Python后端工程师、前端开发都归入技术类产品经理、产品运营归入产品类数据分析师这类岗位我会单独保留因为它和项目主题直接相关也是当前就业市场的热门方向。清洗完的数据我会再做一次统计验证岗位总数据量、缺薪水的记录数量、重复记录占比、各城市分布情况。这些指标能直观反映清洗效果也方便在答辩时展示数据处理的严谨性。2.3 数据入库Django ORM建模与MySQL表结构数据清洗完成后就是入库操作。我在Django项目的models.py中定义了核心数据表这里贴出关键部分from django.db import models class JobPost(models.Model): job_id models.CharField(max_length64, uniqueTrue, verbose_name岗位唯一ID) job_name models.CharField(max_length128, verbose_name岗位名称) company_name models.CharField(max_length128, verbose_name公司名称) industry models.CharField(max_length64, verbose_name行业类别) city models.CharField(max_length32, verbose_name工作城市) salary_min models.FloatField(nullTrue, blankTrue, verbose_name最低月薪(K)) salary_max models.FloatField(nullTrue, blankTrue, verbose_name最高月薪(K)) salary_avg models.FloatField(nullTrue, blankTrue, verbose_name平均月薪(K)) education models.CharField(max_length32, verbose_name学历要求) experience models.CharField(max_length32, verbose_name经验要求) job_category models.CharField(max_length32, verbose_name岗位类别) publish_date models.DateField(nullTrue, blankTrue, verbose_name发布日期) created_at models.DateTimeField(auto_now_addTrue, verbose_name采集时间) class Meta: db_table job_post indexes [ models.Index(fields[city]), models.Index(fields[job_category]), models.Index(fields[industry]), ]MySQL建表时要注意字符集设置为utf8mb4否则存入中文偶尔会出现乱码或Incorrect string value报错。Django的ORM在settings.py里配置数据库连接时加上OPTIONS参数指定字符集比较稳妥。索引设计这块我也踩了一个坑一开始没有给city和job_category加索引后面做统计数据量大时查询很慢接口响应时间到了3秒以上。加上联合索引之后单表几万条数据量级下的统计接口响应时间降到了百毫秒级别。所以千万不要小看索引对可视化项目性能的影响。3. Django后端架构DRF接口设计与数据统计逻辑3.1 Django项目初始化和全局配置后端用的是Django 4.x版本Python 3.10环境。项目初始化命令没什么特别的我重点说一下配置文件里的几个关键点。django-admin startproject employment_analysis cd employment_analysis python manage.py startapp api pip install djangorestframework django-cors-headers pymysql pandas在settings.py中要额外配置三块缺一不可。一是注册rest_framework和corsheaders两个应用二是设置CORS白名单解决Vue开发服务器默认端口8080和Django默认端口8000之间的跨域问题三是配置MySQL数据库连接。INSTALLED_APPS [ django.contrib.admin, django.contrib.auth, django.contrib.contenttypes, django.contrib.sessions, django.contrib.messages, django.contrib.staticfiles, rest_framework, corsheaders, api, ] MIDDLEWARE [ corsheaders.middleware.CorsMiddleware, # ... 其他中间件 ] CORS_ALLOWED_ORIGINS [ http://localhost:8080, http://127.0.0.1:8080, ] DATABASES { default: { ENGINE: django.db.backends.mysql, NAME: employment_db, USER: root, PASSWORD: your_password, HOST: 127.0.0.1, PORT: 3306, OPTIONS: { charset: utf8mb4, } } }不要忘了安装mysqlclient或pymysql这类数据库驱动并且初始化数据库时先手动创建数据库再执行makemigrations和migrate否则Django会直接报Unknown database的错误。3.2 数据统计API接口设计返回结构统一是基础做前后端分离项目接口返回结构的统一性非常重要。我采用的是{code, message, data}三层结构前端axios请求时可以统一拦截处理不需要每个图表单独写错误判断。from rest_framework.views import APIView from rest_framework.response import Response def build_response(data, code0, messagesuccess): return { code: code, message: message, data: data } class SalaryDistributionView(APIView): def get(self, request): category request.query_params.get(category, ) salary_data get_salary_distribution(category) return Response(build_response(salary_data))整个项目一共定义了十几个接口端点按功能可以分成三大类统计概览类/api/overview/summary返回岗位总数、平均薪资、涉及城市数、企业数等核心指标趋势分布类/api/jobs/trend返回按月发布的岗位数量变化/api/jobs/salary-distribution返回薪资区间分布/api/jobs/city-hot返回城市热度排名预测类/api/predict/salary调用训练好的LSTM模型返回未来薪资预测值接口参数的设计上要支持按城市、岗位类别、学历三个维度筛选这样前端才能做联动交互。比如说用户在页面上选择北京 技术类 本科所有图表会同步刷新。实现方式很简单在视图函数里通过filter()动态拼接条件即可。3.3 用Django ORM实现统计聚合逻辑统计接口的核心是用Django ORM的annotate和values做分组聚合。比如统计各城市平均薪资排名我写的查询是这样的from django.db.models import Avg, Count from api.models import JobPost def get_city_salary_rank(): result ( JobPost.objects .exclude(salary_avgNone) .values(city) .annotate( avg_salaryAvg(salary_avg), job_countCount(id) ) .order_by(-avg_salary) ) return list(result)这条查询直接翻译成SQL就是SELECT city, AVG(salary_avg) FROM job_post GROUP BY city ORDER BY avg_salary DESCDjango ORM在背后帮我们完成了所有SQL拼接。几万条数据下这种聚合查询毫秒级就能返回不需要任何额外优化。岗位需求趋势的统计逻辑复杂一点需要按月分组。因为publish_date是日期字段不能直接group by年月我用extra方法配合日期格式化来实现from django.db.models.functions import TruncMonth def get_job_trend(): result ( JobPost.objects .filter(publish_date__isnullFalse) .annotate(monthTruncMonth(publish_date)) .values(month) .annotate(countCount(id)) .order_by(month) ) return list(result)用Django内置的TruncMonth函数截断日期到月份就能轻松实现按月分组的统计。这个方法在Django官方文档里有详细说明属于处理时间序列统计时的标准做法。3.4 中等数据量查询的性能调优记录项目做到后期数据量累计到5万条左右有几个统计接口开始出现响应变慢的情况。我最先想到的是给查询加索引上文已经提到。然后还做了两个优化这里一并写出来供参考。第一个是select_related和prefetch_related的合理使用。虽然这个项目的表结构很扁平没有复杂的外键关联但后面加的SkillDemand表技能需求表关联了JobPost的主键如果不做预取查询每个岗位对应的技能时都会多出一条SQL。用prefetch_related(skills)一次查询就能把数据全部拉出来列表页的查询次数从N1次降到了2次。第二个是接口数据缓存。对于统计类的只读接口我用Django自带的cache框架做了一分钟级别的缓存。热点数据像城市热度排名、平均薪资统计用户访问时直接命中缓存后端压力可以明显降下来。from django.core.cache import cache def get_city_salary_rank_with_cache(): cache_key city_salary_rank data cache.get(cache_key) if data is not None: return data data get_city_salary_rank() cache.set(cache_key, data, timeout60) return data做毕设的同学可以不用缓存但加上这个点答辩时讲到性能优化就有实质内容了这是加分项。4. Vue前端可视化呈现大屏看板组件化体系4.1 Vue项目初始化与Element UI接入前端这块我用的Vue 3 Element Plus ECharts 5的组合。Vue 3的组合式APIComposition API在组织复杂看板逻辑时比Vue 2的选项式API更顺手尤其是多个图表组件都需要请求数据、处理loading状态、响应筛选变化这些场景。初始化命令npm install -g vue/cli vue create employment-frontend cd employment-frontend npm install vue-router4 element-plus axios echarts创建项目时记得选择Vue 3预设路由用History模式还是Hash模式这里建议用History模式虽然部署到服务器需要额外配置nginx但URL看起来干净答辩演示时也更有正式项目的感觉。不过如果打算直接把dist文件扔给别人就能打开那用Hash模式省事。Element Plus组件库是整个前端页面的UI基础。在main.js中全局注册import { createApp } from vue import ElementPlus from element-plus import element-plus/dist/index.css import App from ./App.vue createApp(App).use(ElementPlus).mount(#app)4.2 大屏看板布局设计与路由结构看板页面我采用的是经典的顶部指标卡 中间主图表区 两侧辅助图表的布局方案。顶部的四个指标卡展示核心数据岗位总量、平均薪资、覆盖城市数、需求量最大的岗位类别。中间是核心区放置岗位需求趋势折线图和各行业薪资横向柱状图。两侧分别放置城市热度排名、学历要求分布饼图和技能需求词云。路由规划上总共四个页面/dashboard—— 就业数据总览大屏/analysis—— 交叉分析页面支持多维筛选对比/prediction—— 就业趋势预测页面接入深度学习模型/about—— 项目说明页每个页面拆成独立的Vue组件。组件化是Vue最核心的思想把每个图表封装成单独组件后遇到数据格式问题只需要改一个文件不会影响整页布局。4.3 ECharts图表封装统一配置与数据格式处理ECharts图表初始化和更新是所有页面里复用率最高的逻辑我把这部分抽成了一个通用组件BaseChart.vue。核心思路是组件接收options属性用watch监听options变化后调用setOption更新图表。template div refchartRef :style{height: height px}/div /template script setup import { ref, onMounted, watch, onBeforeUnmount } from vue import * as echarts from echarts const props defineProps({ options: { type: Object, required: true }, height: { type: Number, default: 350 } }) const chartRef ref(null) let chartInstance null onMounted(() { chartInstance echarts.init(chartRef.value) chartInstance.setOption(props.options) window.addEventListener(resize, handleResize) }) watch(() props.options, (val) { chartInstance.setOption(val, true) }) function handleResize() { chartInstance chartInstance.resize() } onBeforeUnmount(() { window.removeEventListener(resize, handleResize) chartInstance chartInstance.dispose() }) /script关于图表配置最重要的一点是务必在setOption时传第二个参数true表示完全替换而不是合并配置。否则图表刷新时可能出现数据残留、图例重复的问题。这个细节是我实际开发时花了半天才排查出来的。前后端数据对接时要注意DRF返回的JSON中日期字段默认格式是2024-01-15这种YYYY-MM-DD格式前端直接放进ECharts的时间轴上是没问题的。但后端返回的聚合指标比如计数、均值经常是Decimal类型JSON序列化后变成数字字符串1250如果不做parseInt转换图表会渲染异常——这个问题在后文的避坑实录里专门讲。4.4 城市热力分布中国地图下钻带来的视觉冲击城市热度地图是大屏上视觉冲击力最强的一块。ECharts提供的地图组件需要单独注册中国地图的GeoJSON数据ECharts 5版本以后地图数据不再打包在核心包里需要额外引入。import chinaJson from /assets/china.json echarts.registerMap(china, chinaJson)有了地图之后用scatter或effectScatter系列把所有城市标注在地图上标注点的颜色和大小用岗位数量映射const mapOptions { tooltip: { trigger: item }, visualMap: { min: 0, max: 500, inRange: { color: [#e0f3f8, #abd9e9, #74add1, #4575b4] } }, series: [{ type: effectScatter, coordinateSystem: geo, data: cityHotData, rippleEffect: { brushType: stroke } }] }这里有一个很实际的经验效果散点图effectScatter加上涟漪动效后用在大屏上比普通scatter好看非常多导致图表的视觉重量感完全不同。每次看板展示时地图这部分都是老师和同学目光停留时间最长的地方。交互设计方面我在地图上绑定了click事件点击某个城市后下方图表会联动显示该城市的岗位分布详情。这样的下钻交互在答辩演示时会很出效果能体现出你确实在设计一个可用的数据分析工具而不只是静态展示几张图。5. 深度学习就业预测模块LSTM时序模型从训练到落地5.1 为什么选LSTM而不是其他算法就业趋势预测这个模块我最初考虑了多种方案线性回归、ARIMA时间序列模型、随机森林、LSTM。最后选LSTM是因为这个任务的核心是根据历史岗位发布序列预测未来趋势本质上是时间序列预测问题。LSTM在处理长距离依赖和时序特征方面有天然优势而且深度学习算法本身就是题目中的关键词用LSTM能让项目的技术含量上一个台阶。用TensorFlow/Keras搭建模型很简单几行代码就能定义一个两层的LSTM网络from tensorflow.keras.models import Sequential from tensorflow.keras.layers import LSTM, Dense, Dropout from tensorflow.keras.callbacks import EarlyStopping def build_lstm_model(input_shape): model Sequential([ LSTM(64, return_sequencesTrue, input_shapeinput_shape), Dropout(0.2), LSTM(32, return_sequencesFalse), Dropout(0.2), Dense(16, activationrelu), Dense(1) ]) model.compile(optimizeradam, lossmse, metrics[mae]) return model模型结构不算复杂但对于毕设项目来说完全够用了。我真不建议一上来就搞Transformer或者注意力机制数据量不够就几万条岗位数据汇总成按月序列模型效果很容易过拟合且调参成本高答辩时也说不清楚。5.2 数据预处理与时间序列窗口构造LSTM需要的是序列数据不能直接把散点数据丢进去。我的处理方式是按月份聚合出每个月的平均薪资和岗位总数得到一个时间序列然后用滑动窗口的方式构造训练样本。import numpy as np def create_sequences(data, seq_length6): x_data, y_data [], [] for i in range(len(data) - seq_length): x_data.append(data[i:i seq_length]) y_data.append(data[i seq_length]) return np.array(x_data), np.array(y_data)这里的窗口大小seq_length6表示用过去6个月的数据预测下一个月这个参数可以根据自己的数据量调整。数据量越大可以适当增加窗口长度但窗口太长会导致训练样本数量急剧减少。特征归一化这一步不能省特别是薪资数据范围在10K到30K之间不归一化直接输入LSTM训练过程很容易不收敛。我用的是MinMaxScaler把所有特征缩放到0到1区间from sklearn.preprocessing import MinMaxScaler scaler MinMaxScaler(feature_range(0, 1)) scaled_data scaler.fit_transform(original_data.reshape(-1, 1))预测结果出来后再用scaler.inverse_transform还原成真实薪资数值才能返回给前端展示。5.3 训练过程与效果评估训练集和测试集按8比2划分但注意时间序列不能乱序划分要按时间顺序用前80%的数据训练、后20%的数据验证。我这里记录一下实际跑出来的效果模型在测试集上的MAE平均绝对误差大概在1.2K左右也就是说预测的平均薪资与真实值偏差约1200元对月度趋势预测来说属于可以接受的范围。history model.fit( x_train, y_train, epochs100, batch_size16, validation_data(x_test, y_test), callbacks[EarlyStopping(patience10, restore_best_weightsTrue)] )EarlyStopping是个好东西它会监控验证集损失如果连续10个epoch没有下降就自动停止训练并恢复到最佳权重有效避免了过拟合。训练完成后直接model.save(salary_predict.h5)保存模型后面Django调用时加载这个文件即可。5.4 模型部署与前端预测页面的联动模型的落地方式是训练结束后保存h5文件然后在Django视图函数中加载模型做预测。这样不用每次预测都重新训练响应速度快很多。import pickle import numpy as np from django.http import JsonResponse model load_model(ml/salary_predict.h5) with open(ml/scaler.pkl, rb) as f: scaler pickle.load(f) def predict_salary(request): recent_data get_recent_salary() scaled scaler.transform(recent_data) sequence scaled[-6:].reshape(1, 6, 1) scaled_pred model.predict(sequence) pred_salary scaler.inverse_transform(scaled_pred)[0][0] return JsonResponse({predicted_salary: float(pred_salary), unit: K})前端预测页面拿到返回的数值后用ECharts画一条历史薪资曲线 未来预测点的组合图。实际折线部分用实线预测部分用虚线这种呈现模式在学术报告和商业产品里都很常用也能很直观地展示模型预测的延伸趋势。前端调用时还要加一个最短请求间隔的控制不能用户在页面上疯狂点击预测按钮导致模型反复推理把后端CPU打满。我用的是Element Plus的Button loading状态控制请求发出后按钮置灰响应完成后恢复。6. 毕设联调避坑实录从数据库到图表的完整排错过程6.1 Django ORM查询慢的元凶缺失索引与N1查询项目联调阶段我遇到的第一个大问题是统计数据接口响应太慢最慢的接口跑了4秒多。用Django-Debug-Toolbar一查看SQL执行情况发现两个问题。第一个问题是job_post表虽然有5万条数据但publish_date字段没有索引导致按月份分组统计时全表扫描。解决方案是给publish_date加上索引执行时间从4.2秒降到了0.3秒。from django.db import models class JobPost(models.Model): # 原有字段... class Meta: indexes [ models.Index(fields[publish_date]), models.Index(fields[city, job_category]), ]加完索引后记得执行python manage.py makemigrations python manage.py migrate。第二个问题是查询技能需求数据时循环里执行了job.skills.all()导致N1查询问题。5万条数据如果每条都查一次技能表那就是5万次数据库查询性能绝对崩。用prefetch_related解决后查询次数降为2次时间从几秒降到了毫秒级。6.2 Decimal类型序列化导致的前端图表异常这个问题非常隐蔽排查过程让我印象很深。某个图表接口的返回JSON是这样的{job_count: 3860, avg_salary: 16.38}数值全部变成了字符串ECharts拿到字符串去映射坐标轴时部分数据会渲染异常更麻烦的是当图表值较大时还可能触发排序错误或轴刻度错乱。根因是Django的Decimal类型字段在DRF序列化时默认转成了字符串。解决方案是在序列化器里显式声明返回类型或者取出数据时统一转成float# 序列化器写法 from rest_framework import serializers class SalaryRankSerializer(serializers.Serializer): city serializers.CharField() avg_salary serializers.FloatField() job_count serializers.IntegerField()所以后来我在统一封装统计函数时所有聚合查询的结果都会做一个类型转换处理确保返回给前端的数据是Python原生的float和int类型JSON序列化后就自然是数字而不是字符串。6.3 前端ECharts图表白屏的排查链路图表白屏是可视化项目里最常见的bug之一我遇到的场景是接口数据明明返回了控制台也没有报错但图表就是显示不出来。排查过程是这样的先在浏览器Network面板确认接口数据格式发现数组字段名是month和count但ECharts的series data需要的是[{name: month, value: count}]格式。问题出在前端组件里没有做字段映射转换。ECharts的柱状图、饼图、折线图对data数组的数据格式要求各不相同。柱状图支持数组格式[Mon, Tue, ...]直接映射类目轴但饼图必须用[{name: ..., value: ...}]的对象数组格式。如果后端返回的字段名是counts而不是valueECharts就会渲染为空。我最后封装了一个统一的格式化函数把后端原始数据转换成各种图表类型所需的格式前端各组件调用这个函数后再喂给ECharts。这样就能一次性规避大部分图表数据格式问题。6.4 LSTM模型预测结果与前端展示的数据衔接最后一个坑出现在模型预测与前端展示的衔接上。LSTM预测出来的薪资是K为单位的一个浮点数比如18.75。前端需要用这个数字去画预测曲线。但如果历史数据是JAVA薪资35.8K预测数据是18.75K两条曲线直接拼接会显得很突兀。我的处理方案是把历史数据的最后6个月也传给前端前端画图时把历史序列和预测点放在同一个series里用不同的lineStyle区分。这样图表上既有历史的真实曲线又能在末端看到预测的延伸趋势视觉上形成一条完整的曲线看起来非常专业。另外模型预测本身会有波动为了让结果更稳健我实际部署时用了多次预测取平均值的方法用最近3个月的序列分别预测下个月然后取中位数作为最终输出。这个方法不复杂但能明显减少单次预测的偶然偏差。这个项目从选题到答辩前后花了将近4个月时间。回头复盘最深的体会是毕设的价值不在于用多高深的算法而在于把一条完整的技术链路走通。爬虫、清洗、存储、接口、可视化、模型预测每一步都有大量细节把这些细节逐个解决本身就是最大的收获。给即将做类似题目的同学一个建议不要等到所有模块写完再联调。我自己的节奏是每完成一个接口马上用前端页面去调通它这样问题能被限制在最小的范围内排查成本也最低。数据可视化项目最忌贪大求全先把核心链路跑通再逐步扩展功能你会发现自己比想象中更能抗事。