简介这份基于Python构建的股市情感分析项目恰逢毕业设计、课程设计与项目开发需求提供从股评语料处理、情感分类模型训练到情绪指数计算、股市行情图绘制的完整方案。项目围绕情绪与股市的关系展开采用标注语料分析股评情感构建情绪指标并研究其与行情走势的关联适合金融数据挖掘、自然语言处理方向的入门与进阶实践。资源包共13个文件约24.73MB主体包括4个Python脚本模型训练、情感指数计算、行情绘图、3个txt语料与说明、2个csv得分数据、2个png行情分析图、1个zip原始数据包以及1个md文档目录结构清晰便于按流程复用。目前已有236人浏览学习。源代码经过严格测试可按model_ml.py、compute_sent_idx.py、plot_sent_idx.py的顺序运行快速复现完整实验配套文档、项目解析与数据模型能有效帮助理解每个模块的设计思路也方便在其基础上扩展应用到真实市场数据中。1. 基于Python的股市情感分析到底在分析什么股市情感分析这个项目说白了就是把“股吧里骂庄家的帖子”“新闻里对降准的评论”“雪球上喊牛市来了的声音”这些非结构化文本用Python抓下来、洗干净再通过数据模型算出一个情绪分数最后把这个分数和K线图放在一起看。它解决的核心问题不是预测明天涨跌而是把“市场情绪”这个玄学概念变成可量化、可回测、可写进毕业论文的数据。适合谁正在选毕业设计题目的计算机或金融专业学生想给自己的课程设计加一个完整闭环的开发者以及想快速验证“情绪到底有没有用”的个人投资者。这个项目的含金量在于它把爬虫、文本处理、机器学习、可视化、前后端串成了一条完整链路是你一个人能独立拿下、答辩时又能讲清楚深度的选题。2. 情感分析的数据模型从文本到情绪分数的三套方案2.1 词典法模型用金融词典给每条新闻打分最朴素的模型不需要任何训练样本你把文本拆成词然后查一个叫“金融情感词典”的列表正向词记1负向词记-1最后算一个总分。这个做法在课程设计里完全够用因为它的逻辑透明——老师说“你说说这个分数怎么来的”你三句话能解释清楚如果换成深度学习你反而说不清。落地的时候我一般用snownlp快速出结果但snownlp的默认模型是商品评论训练的不是金融语料所以分数会偏中性。更稳的做法是直接用 jieba 分词 自定义词典import jieba import pandas as pd # 自定义金融情感词典 positive_words {利好, 上涨, 突破, 增持, 涨停, 超预期, 买入} negative_words {利空, 下跌, 减持, 跌停, 爆雷, 违约, 亏损, 退市} def rule_score(text): if not isinstance(text, str): return 0 words jieba.lcut(text) score 0 for w in words: if w in positive_words: score 1 elif w in negative_words: score - 1 return score # 示例跑通最小闭环 for t in [公司发布业绩预增公告股价涨停, 控股股东减持市场担忧违约风险]: print(t, -, rule_score(t))这段代码的逻辑很简单先把文本分词然后逐个词查词典正向词加一分负向词减一分。三个细节需要注意第一词典要自己维护不能只用通用情感词典因为“减持”在通用词典里是中性词在股市语境里是负向第二否定词没处理比如“不会减持”会被算成负向后边要做否定反转第三分数没归一化长文本和短文本不可比常见做法是除以文本长度或只保留出现次数。如果你要放进报告里建议把规则法的准确率、精确率、召回率跑出来写进文档同时和下一节的机器学习模型做对比这正好是毕设里要求的“方法对比”。2.2 传统机器学习模型TF-IDF 分类器规则法的天花板很低因为股市里的表达太绕了“冲高回落”“缩量下跌”这种词单纯查词典会误判。更常见的方案是把文本转成向量然后用逻辑回归或支持向量机做三分类——乐观、中性、悲观。这时候你需要一份标注好的训练数据这是整个项目里最耗时的部分。标注数据没有捷径。你可以从数据商买也可以用一种半自动的办法用有涨跌幅对应的历史帖子做弱标签比如当天涨幅超过3%的帖子标记为正向跌幅超过3%的标记为负向然后人工抽查清洗。下面是用 scikit-learn 跑通流程的代码from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.linear_model import LogisticRegression from sklearn.pipeline import Pipeline from sklearn.model_selection import train_test_split import pandas as pd df pd.read_csv(labeled_news.csv) x_train, x_test, y_train, y_test train_test_split( df[text], df[label], test_size0.2, random_state42, stratifydf[label] ) model Pipeline([ (tfidf, TfidfVectorizer(tokenizerjieba.lcut, ngram_range(1, 2), max_features5000)), (clf, LogisticRegression(max_iter1000, C1.0)) ]) model.fit(x_train, y_train) print(训练集得分:, model.score(x_train, y_train)) print(测试集得分:, model.score(x_test, y_test))TfidfVectorizer 里tokenizerjieba.lcut是把中文分词交给 jiebangram_range(1,2)会保留相邻两个词的组合这样“业绩预增”这种搭配能进特征。max_features5000限制特征数量避免矩阵爆炸。逻辑回归的C1.0是正则化强度调小一点能抑制过拟合。测试集得分如果能到0.75以上这个模型已经足够写进论文了。这个方案的核心参数就三个最大特征数、正则化系数C、ngram范围。我通常先固定 ngram 为(1,2)然后网格搜索max_features在 [2000, 5000, 10000] 里、C在 [0.1, 1, 10] 里的表现。注意random_state必须固定否则每次跑的分都不同答辩的时候会尴尬。2.3 深度学习模型BERT 微调的门槛与收益如果你想让项目看起来更高级可以用 Hugging Face 的transformers微调一个中文 BERT 模型做情感分类。但这里有个现实问题毕业设计答辩现场评委问“你的训练数据哪来的”“调了哪些超参数”你如果答不上来反而扣分。BERT 的收益是准确率通常比 TF-IDF 高5到10个点代价是它吃标注数据、吃显存、吃时间。如果你的电脑没有 NVIDIA 显卡用 CPU 训练一个中文 BERT 的 epoch 可能要跑几个小时。所以我一般建议毕设里把 BERT 作为对比模型展示不作为主模型。主模型用逻辑回归跑得快解释性强。如果你坚持要跑 BERT最小代码是from transformers import AutoTokenizer, AutoModelForSequenceClassification, Trainer, TrainingArguments tokenizer AutoTokenizer.from_pretrained(bert-base-chinese) model AutoModelForSequenceClassification.from_pretrained(bert-base-chinese, num_labels3) args TrainingArguments( output_dir./checkpoints, num_train_epochs3, per_device_train_batch_size8, learning_rate2e-5, logging_steps100, save_strategyno ) # 这里需要把 df[text] 转成 dataset 对象此处省略完整数据加载 trainer Trainer(modelmodel, argsargs, train_datasettrain_dataset, eval_dataseteval_dataset) trainer.train()learning_rate2e-5是 BERT 微调的标准值太大容易灾难性遗忘太小收敛慢。num_train_epochs3对小数据集也够用跑多了会过拟合。注意save_strategyno是不保存中间检查点省磁盘空间。实际跑之前你要用 tokenizer 把文本转成input_ids, attention_mask这是新手最容易卡住的地方——训练集不能用 pandas 直接塞进 Trainer需要包一层 Dataset。数据模型这一章总结一下词典法适合快速出结果、逻辑透明TF-IDF 加逻辑回归是性价比之王也是我最推荐的主模型BERT 适合做对比实验。数据模型设计的本质是把文本映射成数值后续的行情分析图、回测都建立在这套数值之上。3. 数据采集与清洗把股吧评论和新闻变成结构化表格3.1 数据源与合规获取API、爬虫与公开数据集股市情感分析的数据源大体分三类新闻资讯、股吧帖子、社交媒体。新闻资讯可以用 Tushare Pro、东方财富的公开接口股吧帖子一般靠爬虫公开数据集则需要去 GitHub 或高校数据平台找历史标注语料。这里要提醒一句个人做课程设计爬虫抓公开页面是合规边界内的但不要大规模高频请求更不要抓需要登录或付费的内容。我常用来演示的数据获取方式如下import requests import pandas as pd # 使用 Tushare Pro 获取新闻或公告数据需注册token # ts.set_token(你的token) # pro ts.pro_api() # df_news pro.news(src, start_date20230101, end_date20231231) # 先用简单的东方财富新闻接口做示例 url https://np-listapi.eastmoney.com/comm/web/getNewsByColumns params {client: web, biz: web_news_col, column: 78, order: -date, pageSize: 20} headers {User-Agent: Mozilla/5.0} resp requests.get(url, paramsparams, headersheaders) if resp.status_code 200: data resp.json() print(data.get(data, [])[:2])这里没有直接解析成 DataFrame因为不同接口的返回结构差异很大先打印出来看字段再写解析。新手最容易犯的错是默认接口返回的是 JSON实际东方财富有些接口返回的是 JavaScript 变量包裹的文本需要resp.text剥离前缀。所以我说“先打印再分析再写解析函数”。如果你想做完整的行情关联还要去拉 K 线数据。Tushare Pro 的daily接口能拿到日线行情的开高低收这是后面画图的基础。拿到行情后把新闻/帖子按股票代码和时间与行情对齐。3.2 清洗流程去重、去噪、分词与中文编码爬下来的文本脏得超乎想象重复转载、广告、表情符号、HTML标签、没意义的“支持一下”。清洗流程必须固定否则模型效果会被噪声拉低。我的标准流程分四步去HTML、去空白、去重复、去短文本。import re import hashlib def clean_text(raw): # 去HTML标签 text re.sub(r[^], , raw) # 去URL和emoji text re.sub(rhttps?://\S, , text) text re.sub(r[\U0001F000-\U0001FFFF], , text) # 去多余空白 text re.sub(r\s, , text).strip() return text def drop_duplicates_by_hash(df, coltext): df[_hash] df[col].apply(lambda x: hashlib.md5(x.encode(utf-8)).hexdigest()) df df.drop_duplicates(subset_hash) df df.drop(columns_hash) return df # 删除长度小于10个字符的垃圾文本 df[clean_text] df[raw_text].apply(clean_text) df df[df[clean_text].str.len() 10]drop_duplicates_by_hash比直接用df.drop_duplicates可靠因为转载的帖子可能带不同的链接后缀或时间戳哈希能把这些差异抹掉一部分。真正的语义重复比如“同一个新闻换了标题重复发”还需要用编辑距离或 simhash 去重毕设里做基础的 md5 去重就够但答辩时你要能说出来“我做了哈希去重还有更复杂的 simhash 方案留作改进方向”。清洗后必须统一编码。有人把数据存成 Excel 再读结果中文变成乱码原因就是编码没写对。我建议所有中间文件统一用 UTF-8 保存读和写都显式指定encodingutf-8。同时把清洗前后的文本长度、去重数量统计好这个数字写进论文是很有说服力的实验过程记录。3.3 数据模型设计情绪表、股票表、行情表的关系既然标题里强调“数据模型”就说明项目不能只有一堆散乱的 CSV。你要在文档里给出数据库表设计这也是毕业设计系统设计章节的核心内容。我用 SQLite 做演示因为不需要安装数据库服务答辩时可以直接跑CREATE TABLE stock ( code TEXT PRIMARY KEY, name TEXT NOT NULL, industry TEXT ); CREATE TABLE news ( id INTEGER PRIMARY KEY AUTOINCREMENT, code TEXT NOT NULL, title TEXT, content TEXT, publish_time DATETIME, source TEXT, sentiment_score REAL, label INTEGER, -- 0悲观 1中性 2乐观 FOREIGN KEY (code) REFERENCES stock(code) ); CREATE TABLE daily_kline ( code TEXT NOT NULL, trade_date TEXT NOT NULL, open REAL, high REAL, low REAL, close REAL, volume BIGINT, PRIMARY KEY (code, trade_date) ); CREATE TABLE daily_sentiment AS SELECT code, date(publish_time) AS trade_date, COUNT(*) AS n_items, AVG(sentiment_score) AS avg_sentiment FROM news GROUP BY code, date(publish_time);这个模型设计的关键在于daily_sentiment是聚合表它把细粒度的新闻表压缩成“每天每个股票的平均情绪”这样才能和daily_kline做表连接。不要直接在 news 表里关联行情否则 n 条新闻要 join n 次数据量一大就卡死。视图daily_sentiment在 SQLite 里不能直接建因为带有聚合函数实际可以先用查询语句创建或者直接存成 pandas DataFrame 导出 CSV。毕设里我更建议存成 CSV 或 Parquet 文件因为答辩演示时用 SQLite 相比文件更繁琐。数据模型这一章的核心不是建表而是让你理解“怎么组织数据才能支持后续的图表和回测”。把情绪表设计成按日聚合的宽表后面画图时一行就是一天直接和 K 线并排。4. 股市行情分析图把情绪分数与K线叠在一张图上4.1 情绪总分的时间序列图matplotlib 最小代码先画出情绪分数的日均值折线图这是项目里第一张图也是后面所有图表的基础。这里要注意直接用 matplotlib 跑中文会显示成方框必须配置中文字体。import pandas as pd import matplotlib.pyplot as plt plt.rcParams[font.sans-serif] [SimHei, Microsoft YaHei] plt.rcParams[axes.unicode_minus] False df_sent pd.read_csv(daily_sentiment.csv, parse_dates[trade_date]) fig, ax plt.subplots(figsize(12, 5)) ax.plot(df_sent[trade_date], df_sent[avg_sentiment], color#C62828, linewidth1.5) ax.set_xlabel(日期) ax.set_ylabel(平均情绪分数) ax.set_title(沪深300每日平均情绪词典法) fig.autofmt_xdate() plt.tight_layout() plt.savefig(output/sentiment_series.png, dpi200)parse_dates参数把日期列转成 datetime 类型这样 matplotlib 才能正确画横轴。fig.autofmt_xdate()会自动旋转时间标签避免密集重叠。保存用dpi200是为了满足论文插图分辨率。如果你觉得 matplotlib 太丑可以用 plotly 生成交互图但论文里放静态图交互图放在附录或演示现场。4.2 情绪与涨跌幅的相关性分析散点图热力图光看曲线很难说情绪分数对涨跌有没有解释力所以下一步要把情绪和收盘价、涨跌幅做相关性分析。这里用 pandas 的corr()和pct_change()就能算出关键数据。df_kline pd.read_csv(daily_kline.csv, parse_dates[trade_date]) df pd.merge(df_kline, df_sent, on[code, trade_date], howinner) df[return] df[close].pct_change() * 100 df[sentiment_lag1] df[avg_sentiment].shift(1) corr_same df[avg_sentiment].corr(df[return]) corr_lag1 df[sentiment_lag1].corr(df[return]) print(f同期情绪与收益相关系数: {corr_same:.3f}) print(f滞后一期情绪与收益相关系数: {corr_lag1:.3f}) # 散点图 plt.figure(figsize(6, 6)) plt.scatter(df[avg_sentiment], df[return], alpha0.3) plt.xlabel(当日平均情绪分) plt.ylabel(当日收益率 (%)) plt.title(情绪分数与收益率散点图) plt.grid(alpha0.3) plt.savefig(output/sentiment_return_scatter.png, dpi200)shift(1)是滞后一天的意思把 t 日情绪和 t1 日收益对齐用来检验“今天的情绪能不能预测明天的涨跌”。同期相关系数接近 0 很正常情绪本来就是同向的滞后一期如果略有显着正相关你的故事就成立了一半。注意pct_change()第一行是 NaN合并后别忘dropna()。4.3 输出论文级图表中文字体、坐标轴密度、配色论文图表的底线是清晰、可复现、不失真。很多学生直接截图答辩时被问“图怎么来的”只能说是代码生成的结果现场跑不出来。一定要把图表生成代码放在项目目录里并保证原数据能重新跑出同一张图。我的图纸规范如下尺寸统一 12:5 或 6:6字体大小 10-12颜色用深红和深蓝两色避免花哨坐标轴包括来源说明每个图都有可读的文件名。情绪图和 K 线图叠放在一起时需要两个 y 轴左边是价格右边是情绪分fig, ax_price plt.subplots(figsize(14, 7)) ax_price.plot(df[trade_date], df[close], color#1565C0, label收盘价, linewidth1) ax_sent ax_price.twinx() ax_sent.plot(df[trade_date], df[avg_sentiment], color#C62828, label情绪分数, linewidth0.8, alpha0.7) ax_price.set_ylabel(收盘价) ax_sent.set_ylabel(情绪分数) lines1, labels1 ax_price.get_legend_handles_labels() lines2, labels2 ax_sent.get_legend_handles_labels() ax_price.legend(lines1 lines2, labels1 labels2, locupper left) plt.title(沪深300收盘价与情绪分数叠加图) fig.autofmt_xdate() plt.savefig(output/price_sentiment_overlay.png, dpi200)双 y 轴图是最容易被评委接受的图因为一眼就能看出情绪曲线是否领先价格曲线。如果你发现两条线完全同步就要检查是不是日期没对齐如果情绪线总是提前一两天拐头这就是你的项目最有价值的发现。把这张图和相关系数一起放进论文整个“数据分析”章节就立住了。5. 避坑排查从环境到数据到模型的五个常见翻车点5.1 现象pip install 装不上 jieba 和 snownlp原因Python 版本太新或太老导致轮子冲突经常发生在 3.11 以上的环境里部分包没有预编译版本。解决先升级 pip再装指定版本最后用 conda 兜底。python -m pip install --upgrade pip pip install jieba0.42.1 snownlp0.12.3 # 如果还是失败尝试用 unicode 安装 python -m pip install jieba snownlp -U另外不要用本地 Python 全局环境装实验包建议python -m venv .venv创建虚拟环境。课程设计交付时附上requirements.txt是基本礼貌不然老师换台电脑跑不起来直接影响分数。5.2 现象读取 CSV 显示中文乱码原因文件保存用了 GBK而读取代码指定了 UTF-8或反过来。解决统一使用 UTF-8-SIG 编码写文件读取时显式指定。df.to_csv(data.csv, indexFalse, encodingutf-8-sig) df pd.read_csv(data.csv, encodingutf-8-sig)utf-8-sig会带 BOM 头Excel 打开不乱码pandas 也能正常读。如果你已经产生乱码文件用记事本打开另存为 UTF-8 可救回来一次但如果数据量大就老老实实重新清洗。5.3 现象情感分数全是 0 或 0.5原因词典里的词和语料实际词重叠太少或者 snownlp 对股市文本默认输出中性。解决先统计分词结果和词典的命中率。words set(jieba.lcut( .join(df[clean_text]))) hit words positive_words | words negative_words print(词典命中词数:, len(hit))如果命中数是个位数说明你的词典太薄。我一般从网上公开的金融情感词典合并再往里面补充“背刺”“闪崩”“妖股”“上车”等股市黑话。还有一种坑分词器把“不看好”切成了“不/看好”负向词“不看好”不存在。解决办法是在自定义词典里加短语jieba.add_word(不看好) jieba.add_word(超预期)5.4 现象K线图日期不连续横坐标一团黑原因节假日停牌交易日期本身不连续加上你直接用字符串画横轴。解决把日期列转成 datetime并将画图区间限制为交易日df[trade_date] pd.to_datetime(df[trade_date]) df df.sort_values(trade_date) plt.xticks(df[trade_date][::5], df[trade_date].dt.strftime(%m-%d)[::5])[::5]是每5天标一个刻度。如果你把数据重采样成自然日也可以df.set_index(trade_date).resample(D).ffill()但图表上会出现假的平线。我倾向于不填充保持市场原貌同时在文档里说明缺失日期的原因。5.5 现象测试集准确率 0.8但滞后一天回测结果随机原因在切分训练集时用了随机切分而股票数据是时间序列文本里包含了未来信息或同源信息。解决必须按时间顺序切分不能train_test_split(random_state42)乱序。df df.sort_values(publish_time) split_idx int(len(df) * 0.8) train df.iloc[:split_idx] test df.iloc[split_idx:]时间序列切分的另一个坑是新闻和行情的时间对齐。有些数据源的时间戳是发布日期而不是真实时间导致你用了“第二天的新闻”去预测“当天的涨跌”。我处理这个问题的办法是统一使用date(publish_time)作为交易日并且把新闻时间戳小于当日 15:00 的算当天15:00 之后的算次日。这样能避免盘后消息被错配到已收盘的行情上。这五条坑每一件我都实测踩过前三条是环境问题后两条是方法论问题后者才是真正决定项目分水岭的关键。6. 进阶把情绪分数做成可回测的交易信号6.1 从日频情绪构造情绪因子情绪分数本身噪声很大直接拿它当买卖信号会频繁交易。常见的做法是先做标准化和平滑再把极值当作信号。我用的是 5 日移动平均偏离度df[sentiment_ma5] df[avg_sentiment].rolling(5).mean() df[sentiment_z] (df[avg_sentiment] - df[sentiment_ma5]) / df[avg_sentiment].rolling(20).std() df[signal] 0 df.loc[df[sentiment_z] 1.0, signal] 1 # 情绪过热考虑买入或持多 df.loc[df[sentiment_z] -1.0, signal] -1 # 情绪极差考虑空仓参数5和20是短期均线和长期波动的窗口不是固定真理。我把z 1定为正信号z -1定为负信号这个阈值可以按样本量调整但你要在文档里说明如何网格搜索选的阈值。这一段在答辩里特别加分因为它展示了你不仅有情感计算还知道怎么把情绪分数变成可执行的策略。6.2 用简单回测脚本验证信号收益不需要上 backtrader手写一个简单的逐日回测能算收益率和胜率就足够df[strategy_return] df[signal].shift(1) * df[return] df[strategy_equity] (1 df[strategy_return] / 100).cumprod() df[buyhold_equity] (1 df[return] / 100).cumprod() print(情绪策略总收益:, df[strategy_equity].iloc[-1] - 1) print(买入持有总收益:, df[buyhold_equity].iloc[-1] - 1) # 略过空仓日 active_days (df[signal].shift(1) ! 0).sum() win_rate (df[strategy_return] 0).mean() print(策略胜率:, win_rate)signal.shift(1)很关键它保证用昨天的信号做今天的交易避免未来函数。回测里最大的陷阱是手续费和涨跌停限制——买入当日不能按收盘价成交涨停时买不进跌停时卖不出。毕设可以忽略但文档里要写一句“未考虑交易成本实际应用中需扣除滑点和佣金”。6.3 三个验证技巧滚动窗口、随机基线、样本外测试情绪能不能赚钱不能只看一次回测。我习惯做三件事第一滚动窗口比如每 60 天重新训练或重新确定阈值测其稳定性第二随机基线把信号序列随机打乱 100 次看真实策略收益排在随机分布中的什么位置第三样本外测试用 2023 年之前的数据做参数选择2023 年之后只跑一次作为样本外验证。这三个技巧不复杂但绝大多数同学不会做。你在报告的“实验与分析”章节里写出随机基线的百分位比如“真实策略收益高于 87% 的随机序列”这个数据比堆十个图更有说服力。我也踩过坑某次我把信号对齐错了一天回测收益翻倍看起来完美实际是用了未来数据。后来我强制每次回测前打印df[[trade_date, signal, return]]检查前五行确保信号确实在收益之前。这个检查习惯保持到现在也希望你把它写进自己的项目习惯里。用第一人称说一个教训我第一次做情绪因子回测时看到 5 日胜率 68%差点以为自己发现了印钞机。结果发现我把当日情绪分和当日收益率放在了同一行而情绪分是从新闻里算的新闻可能是盘后发的等于用收盘后信息预测当天收益。改成次日对齐后胜率掉到 52%。从此我把“时间对齐”当作了所有数据实验的第一原则。做这个项目最大的收获不是模型有多准而是你开始对数据来源、时间戳、对齐方式这些细节敏感。希望这篇笔记帮你避开我走过的那些弯路把这个课题做得又快又稳。本文还有配套的精品资源点击获取