ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于Hadoop与PySpark的考研分数线预测及院校推荐系统

基于Hadoop与PySpark的考研分数线预测及院校推荐系统 每年考研出分那几天我都能接到好几个学弟学妹的求助说分数线涨得太离谱了报的学校心里完全没底。这个标题组合在一起实际上就是一个典型的“大数据全栈毕业设计”用Scrapy把考研相关的公开数据抓下来丢进Hadoop做分布式存储再用PySpark做清洗和特征提取最后跑预测模型和推荐算法产出一个既能预测分数线、又能推荐目标院校的系统。适合两类人参考一类是计算机专业要做毕设的学生另一类是刚学完Hadoop和Spark基础、想找个完整项目练手的开发者。这篇文章我会把项目的技术选型、核心实现和踩坑记录完整拆开讲尽量把每一步为什么这么做说清楚。1. 项目全貌与技术选型1.1 先搞清楚这个系统到底在解决什么问题考研选校和分数线预测本质上是一个信息不对称问题。学生能看到的是零散的历年分数、报录比、招生人数但这些数据分散在各个平台格式还不一样。有的学校公布的是总分线有的是单科线有的给了报录比有的只给复试名单年份一多数据口径完全对不上。这个系统的核心价值就是把这些散落的数据采集、清洗、整合成结构化的数据仓库然后基于历史规律做两件事第一预测某个学校某个专业今年的复试分数线大概在什么区间第二根据学生的本科背景、目标地区、期望专业给出一份“冲、稳、保”的院校推荐列表。说白了这不是一个纯算法项目而是一个“数据管道 算法应用”的项目。数据管道的比重占六成以上算法部分反而相对标准。很多做毕设的同学把这个顺序搞反了一头扎进调参里结果数据一塌糊涂模型效果自然稀碎。1.2 为什么选Hadoop PySpark Scrapy这套组合说实话考研数据量远没有到“非用Hadoop不可”的程度。一个MB级的CSV文件用Pandas处理完全够。但这里有个现实问题毕业设计需要体现技术栈的覆盖面和工程难度。Hadoop PySpark这套组合代表了分布式存储和分布式计算两条完整的技术线加上Scrapy爬虫就是“数据采集—数据存储—数据处理—数据应用”的全链路。选这套组合还有几个实际考量。一是Hadoop的HDFS适合存爬虫产出的原始半结构化数据比如JSON、日志文件不需要提前设计表结构这比MySQL友好很多。二是PySpark的DataFrame API和Pandas非常接近学习曲线平缓但写出来的是分布式计算任务可以在简历上写“熟悉Spark大数据处理”。三是Scrapy是Python生态里最成熟的爬虫框架异步并发、中间件、管道这些机制都有现成的扩展开源组件也容易。当然这套组合在真实生产环境里也有合理性。如果数据源扩展到全国所有院校、所有专业、近二十年的数据加上定期增量更新单机内存处理确实会吃力分布式架构的扩展性优势就会体现出来。1.3 整体架构分层设计整个系统我习惯分成四层来看数据采集层用Scrapy爬取目标网站公开数据包括历年复试分数线、国家线、报录比、招生简章、专业目录等爬下来的数据统一转成JSON或CSV格式。存储层用Hadoop HDFS存放原始数据文件再导出结构化数据供上层使用。这里要注意HDFS适合大文件顺序读写不适合放大量小文件所以爬虫产出的文件要做合并。计算层用PySpark做数据清洗、特征工程和模型训练。Spark的MLlib库提供了回归、分类、聚类这些常用算法而且Pipeline机制可以很方便地串联特征处理和模型训练。应用层是Flask或Django写的Web服务提供分数线预测查询和院校推荐的接口前端页面展示结果。这个分层的好处是每一层都可以单独替换。比如爬虫层今天爬研招网明天换一个数据源存储层和计算层不用动预测模型想从线性回归换成随机森林只需要改算法层的代码。2. 数据从哪来Scrapy爬虫的完整设计2.1 爬虫目标与数据字段设计先明确要爬什么数据。考研相关数据主要分几类历年国家线分A区B区、学硕专硕、不同学科门类、院校复试分数线、招生人数、报录比、专业目录、院校基本信息。以院校分数线为例我设计的核心字段包括院校代码、院校名称、省份、城市、院校层次985/211/双一流/普通、专业名称、专业代码、学位类型学术型/专业型、年份、复试总分线、政治单科线、英语单科线、数学单科线、专业课单科线、招生人数、报考人数、录取人数。这里有个容易忽略的坑字段一定要在设计阶段就定好并且保持统一。很多数据源的字段名不一样比如有的叫“复试分数线”有的叫“复试线”有的叫“校线”爬虫里要提前做好字段映射不然后面清洗数据会非常痛苦。2.2 Scrapy项目结构设置Scrapy项目结构很简单命令scrapy startproject kaoyan直接生成项目骨架。核心文件是items.py定义数据结构spiders目录放爬虫逻辑pipelines.py做数据后处理settings.py做全局配置。settings.py里这几个参数是必须调的# 设置合理的请求间隔既减轻对方服务器压力也降低被封IP的风险 DOWNLOAD_DELAY 2.5 # 启用随机UA避免固定User-Agent被识别 USER_AGENT Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 # 遵守robots协议 ROBOTSTXT_OBEY True # 并发数不要贪多爬考研数据这种中小型站点8就够了 CONCURRENT_REQUESTS 8 # 开启重试机制网络抖动时自动重试 RETRY_ENABLED True RETRY_TIMES 3这个配置里最核心的是DOWNLOAD_DELAY。之前有个同学图快设成0.5秒结果爬了不到两分钟IP就进去了整个项目的进度全部打乱。正常爬公开数据2到3秒的延迟是安全区间数据量大就拉长采集时间不要用高并发去赌对方的反爬强度。2.3 数据落盘与Pipeline设计Scrapy的Pipeline是做数据清洗和存储的地方。我的做法是先用Items装载爬下来的原始数据在Pipeline里做基础格式校验然后写入文件。import json class KaoyanPipeline: def open_spider(self, spider): self.file open(kaoyan_data.jsonl, a, encodingutf-8) def close_spider(self, spider): self.file.close() def process_item(self, item, spider): # 基础字段校验脏数据直接丢弃 if not item.get(school_name) or not item.get(major_name): raise DropItem(fMissing required fields: {item}) # 字段类型统一 if isinstance(item.get(total_score), str): item[total_score] item[total_score].strip() line json.dumps(dict(item), ensure_asciiFalse) \n self.file.write(line) return item用JSONL格式每行一个JSON对象而不是JSON数组好处是追加写入方便万一爬虫中断已经写进去的数据不会丢。2.4 增量爬取与去重策略考研数据有年份维度每年3月出国家线各院校陆续公布复试线。如果做的是持续更新的系统Scrapy的增量爬取就很重要。我的方案是用Spider的start_requests里读取已爬取年份列表只请求缺失年份的URL同时在items.py里用请求指纹做去重或者用scrapy-deltafetch这个中间件。更简单的做法是每轮爬取前对比数据库里已有的最大年份只爬新数据。比如当前库里有2023年的数据那爬虫就从2024年往后再爬。这个方法实现简单对毕设来说完全够用不需要引入复杂的消息队列。3. 大数据底座Hadoop存储与PySpark计算3.1 Hadoop环境搭建的要点环境搭建是整个项目里最劝退的部分也是报错最多的地方。用伪分布式模式即可一台虚拟机或者云服务器就能跑不要一上来就搞三台集群伪分布式搭建好了原理通了三台就是配置文件的复制粘贴。Hadoop核心配置文件有三个core-site.xml配置NameNode地址hdfs-site.xml配置副本数和数据目录yarn-site.xml配置资源调度。!-- core-site.xml -- configuration property namefs.defaultFS/name valuehdfs://localhost:9000/value /property /configuration !-- hdfs-site.xml -- configuration property namedfs.replication/name value1/value /property /configuration注意伪分布式模式下副本数必须设为1因为只有一台DataNode默认副本数3会导致文件一直处于副本不足状态上报的块信息永远不正常。启动顺序也很重要很多新手一上来就start-all.sh报错了也不知道从哪看。正确的顺序是先格式化NameNode然后依次启动hdfs namenode -format start-dfs.sh start-yarn.sh jpsjps命令输出里能看到NameNode、DataNode、SecondaryNameNode、ResourceManager、NodeManager这五个进程缺一个就用对应日志排查。3.2 爬虫数据接入HDFS爬虫产出的JSONL文件怎么进HDFS直接用命令行上传最省事hdfs dfs -mkdir -p /user/kaoyan/raw hdfs dfs -put kaoyan_data.jsonl /user/kaoyan/raw/这里要注意HDFS的小文件问题。爬虫是按批次产出文件的一段时间就会产生一堆几十KB的小文件。HDFS对大量小文件支持不好NameNode内存会被文件元数据占满。解决方式有两种一是定时用hdfs dfs -appendToFile把多个小文件合并成大文件二是在上传前本地就用Python脚本合并成一个或几个大文件。亲测推荐第二种方式简单可控不依赖额外的定时任务。爬虫每完成一轮采集本地脚本就把该轮所有JSONL文件合并成一个有时间戳的大文件再上传HDFS。3.3 PySpark数据清洗与特征工程数据进入HDFS以后就轮到PySpark干活了。先读出原始数据做清洗然后保存成Parquet格式Parquet列式存储格式查询效率远高于JSONL和CSV。from pyspark.sql import SparkSession from pyspark.sql.functions import col, when, isnan, isnull spark SparkSession.builder \ .appName(KaoyanDataClean) \ .getOrCreate() df spark.read.json(hdfs://localhost:9000/user/kaoyan/raw/*.jsonl) # 去掉完全重复的记录 df df.dropDuplicates([school_name, major_name, year]) # 分数线处理有些是空值有些是字符串无或—— df df.filter(~col(total_score).isin([无, ——, ])) df df.filter(col(total_score).isNotNull()) # 类型转换 df df.withColumn(total_score, col(total_score).cast(int)) print(f清洗后数据量: {df.count()}) df.write.mode(overwrite).parquet(hdfs://localhost:9000/user/kaoyan/clean/)这套代码里最关键的思想是“先看后算”在真正动手写清洗逻辑之前先花时间用DataFrame的describe、distinct这些动作摸清数据的分布情况看看哪些字段缺失最多哪些字段有奇怪的异常值。很多新手上来就写过滤逻辑结果误伤了有效数据或者漏掉了脏数据。特征工程是为了给预测模型用的。从清洗后的数据里可以构造这些特征年份的序号编码比如2018年记为0每年加1让模型学到时间趋势院校层次编码985/211/双一流/普通分别映射为数值地区热度等级按报考热度给省份分等级上一年度的分数作为滞后特征该专业近三年的平均分数和标准差基于内容推荐是标配做法因为考研数据不会像电商那样有大量用户评分记录很难做真正的协同过滤所以院校推荐系统更实用的是多因子加权评分。4. 核心算法分数线预测与院校推荐4.1 分数线的预测思路分数线预测有三种常用思路我在项目里分别尝试并做了对比。第一种是线差法这是考研机构最常用的方法。线差等于院校分数线与国家线之间的差值比如某校计算机专业2023年复试线是330分当年国家线是273分线差就是57分。统计最近三到五年的线差取加权平均再叠加当年的国家线预测值就是该校的预测分数线。第二种是时间序列法把历年分数线按时间排序用简单指数平滑或者ARIMA模型预测下一期数值。这种方法的局限性是样本量太少大多数专业只有四五年有效历史数据时间序列模型的统计意义有限。第三种是机器学习回归用随机森林或者梯度提升树把特征设为年份、地区热度、招生人数、报考人数、院校层次等目标是预测分数线。数据量够的时候效果最好能捕捉到多维度的非线性关系。我实际采用的方案是把线差法和机器学习做一个融合。用线差法算出一个基准预测再用随机森林预测出修正量两者相加作为最终结果。这是一种很实用的集成思路比单独用任何一种都稳定。4.2 PySpark MLlib训练随机森林模型PySpark的训练代码比较标准关键是Pipeline机制。把特征向量化和模型训练串在一条流水线里。from pyspark.ml.feature import VectorAssembler from pyspark.ml.regression import RandomForestRegressor from pyspark.ml import Pipeline from pyspark.ml.evaluation import RegressionEvaluator feature_cols [ year_seq, school_level, hot_degree, enroll_count, apply_count, last_year_score ] assembler VectorAssembler(inputColsfeature_cols, outputColfeatures) rf RandomForestRegressor( featuresColfeatures, labelColtotal_score, numTrees100, maxDepth6, seed42 ) pipeline Pipeline(stages[assembler, rf]) train_df, test_df clean_df.randomSplit([0.8, 0.2], seed42) model pipeline.fit(train_df) predictions model.transform(test_df) evaluator RegressionEvaluator( labelColtotal_score, predictionColprediction, metricNamermse ) rmse evaluator.evaluate(predictions) print(fRMSE: {rmse:.2f})用这个模型之前我建议先跑一个线性回归做baseline如果随机森林和线性回归的结果差别不大说明数据里没有什么复杂的非线性关系用线差法就够反而更稳。机器学习不是用来炫技的而是用来解决问题。4.3 院校推荐系统的评分公式推荐系统的核心是一个可解释的评分函数。用户输入目标专业、地区偏好、可接受的城市范围之后系统给每个候选院校打分排序综合评分 0.35 × 录取概率得分 0.25 × 院校层次得分 0.2 × 地域偏好得分 0.2 × 学科实力得分录取概率得分用预测分数线和用户预估分的关系计算。如果预估分高于预测分数线加一个安全冗余得分就高属于“稳”如果预估分在预测线边缘得分中等属于“保”还是“冲”要看超出程度。这里要讲清楚一个业务逻辑什么叫冲稳保。冲就是预估分略低于预测线的院校录取概率在0.2到0.4之间有一搏的价值稳是预估分高出预测线10分到15分录取概率在0.5到0.7之间保是预估分高出20分以上录取概率在0.8以上大概率能进的院校。院校层次得分和地域偏好得分本质上是一个策略输入。有的学生非985不去有的学生想留在家乡省份这些应该做成可配置的权重。4.4 推荐结果的几种实现方式推荐结果的筛选逻辑分为硬性过滤和软性排序。硬性过滤在前先把不符合硬性条件的院校剔除比如专业名称不匹配、所在省份不在用户选择范围内软性排序在后用评分公式对剩余院校排序。这样可以避免一个常见问题综合评分很高的学校但它的目标专业根本没有硕士点这就是硬性过滤没做干净的典型表现。从工程实现角度推荐服务可以单独封装成一个Python模块接收用户参数调用预测模型的输出结果计算评分后返回Top10。评分公式里的权重建议做成可配置的字典后续要调整权重不用改代码。5. 实操复盘我从这个项目里踩过的坑5.1 环境与部署类问题Hadoop启动失败的排查思路我总结成一句口诀“先看日志再看端口最后看权限”。NameNode起不来大概率是格式化不彻底或者元数据损坏DataNode起不来大概率是clusterID不一致ResourceManager报错先去看YARN的日志文件里面会直接告诉你什么内存参数不合法。PySpark常见的OOM问题多半不是数据真的太多而是内存配置没过脑子。具体来说就是executor内存、driver内存、并行度这三个参数没有配合调整。在伪分布式环境里Spark任务要注意限制executor数量比如spark-submit --master local[2] \ --executor-memory 2g \ --driver-memory 2g \ clean_and_train.py本地模式local[2]就够测试用不要贪多。Spark的内存模型里有执行内存和存储内存的动态占用机制最直白的经验是给executor留30%以上的空闲内存GC的负担会小很多。5.2 爬虫与数据类问题爬虫最常用的反爬方案是代理IP池和浏览器指纹伪装。但是对毕设这种量级的爬取需求下载延迟加随机UA就够了没必要上代理池。把大量时间花在打造一个工业级反爬系统上本质上是过度设计。先说结论数据完整性的优先级高于数据量。有个印象很深的坑某学校某年的分数线数据存在专业目录PDF里Scrapy直接抓HTML解析不到。当时用Selenium模拟浏览器打开页面再用PDF解析库提取表格才把数据补齐。这就牵出一个考研数据的典型问题数据格式不统一HTML、PDF、图片都可能藏着数据采集时必须分类处理。数据清洗阶段最常见的问题是编码乱码。处理中文数据时存储层统一用UTF-8数据库连接串里也明确指定编码基本上能避开大部分乱码问题。5.3 建模与业务类问题预测模型一个新手上路最容易犯的错误是把预测分数当成了一个“唯一精确答案”。实际上分数线的波动受很多因素影响试题难度、当年报考人数、临时扩招政策这些都不可能完全建模出来。所以我在项目里有意把预测结果设计成一个区间而不是一个点值比如预测结果330分置信区间是±8分代表这个学校分数线落在322到338之间的可能性是85%。这个设计更符合实际决策需要。业务层面还有一个非常现实的问题数据量太小导致推荐结果不可信。如果某个学校某个专业只有一年的数据预测模型给出的结果就没什么参考价值。我在推荐评分里加了一个惩罚项数据年份不足3年的院校综合评分乘以0.9的置信系数。这个处理虽然简单粗暴但非常有效能避免把垃圾数据推荐给用户。很多商用系统的冷启动问题也是这么处理的——用数据量来对结果降权。5.4 常见问题速查表问题现象常见原因解决方案Hadoop NameNode无法启动格式化过程出错或元数据损坏检查日志备份后重新格式化NameNode目录DataNode启动后自动退出clusterID与NameNode不一致删除DataNode数据目录重新格式化或手动重置clusterIDSpark任务报OOMexecutor内存不足或并行度过高调整spark.executor.memory降低并行度改用本地模式测试Scrapy爬取被拒绝请求频率过高或UA指纹被识别增加DOWNLOAD_DELAY使用随机UA检查robots协议中文乱码编辑器和数据存储编码不一致统一使用UTF-8保证Spark配置字符集为UTF-8预测结果全是平均值特征里没有有效信息或样本量太小补充滞后特征检查是否存在数据泄漏简化模型这些坑我整理成速查表是希望后来者少走弯路。另一个小建议是每天做一次数据备份Polyspace一次意外就全没了代价是用一个通宵重新搭环境。最后分享一点个人的实操体会做了这个项目之后我最大的感受是毕业设计不是算法竞赛不是模型越高级越好而是工程链路的完整体现。一个技术方案好不好要看数据能不能顺畅地从爬虫管道流到预测模型再流到用户界面。我见过太多同学把大量时间花在调参上结果数据清洗只写了三行代码最后模型的输入全是垃圾数据输出自然就变成垃圾了。如果你正准备复刻这个项目我的建议是先用一个周末跑通全流程的最小版本哪怕预测模型直接用平均数也要先把数据管道打通后面再逐步替换掉各个部分。这种从下到上开发的好处是任何时候整个系统都是可运行的调试起来不会一片漆黑。最后再分享一个小技巧项目里的所有配置项包括Scrapy的下载延迟、推荐算法的权重、Spark的内存参数全部抽到单独的配置脚本里不要散落在代码里。前期可能觉得多此一举但等你开始调参的时候就会感谢这个决定。调参是高频操作能快速修改并立即看到效果整个开发效率会有质的提升。
返回列表