ARTICLE DETAIL

资讯详情

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

Python股市情感分析:从新闻数据到K线情绪可视化的完整链路

Python股市情感分析:从新闻数据到K线情绪可视化的完整链路 简介基于Python的股市情感分析项目面向毕业设计、课程设计及项目开发的学习者利用互联网股评语料量化投资者情绪并构建情绪指标研究其与股市行情的关系。完整源码经过严格测试可按model_ml.py、compute_sent_idx.py、plot_sent_idx.py的顺序运行依次完成情感分类模型训练、情绪指数计算与行情对比可视化适合在此基础上扩展情感因子选股等方向。包体共13个文件涵盖Python脚本、文本说明、CSV数据、可视化图表及Markdown文档其中脚本用于模型构建与绘图txt和md提供项目解析csv包含标注语料与情感得分png为结果图整体压缩包24.73MB目录清晰便于查阅。目前已有236人学习参考可帮助读者理解从文本情感分析到投资决策的完整链路适用于课设展示、毕设答辩或实际项目开发。1. “Python 股市情感分析”到底解决什么问题从一条新闻到一个可解释的行情结论用 Python 做股市情感分析核心是把新闻、公告、股吧评论这类非结构化文本用自然语言处理模型映射成一个 0 到 1 之间的情绪分数再和 K 线、成交量、涨跌幅放到同一张行情分析图里对照。标题里的“数据模型”负责承接文本与分数“股市行情分析图”负责把结果交付成图整套链路刚好覆盖爬虫、清洗、建模、可视化四个阶段所以经常被选作毕业设计、课程设计和项目开发练手。它不承诺预测涨跌而是把“市场情绪”变成一条有时间戳的曲线供你做投资研究、复盘或演示。适合 Python 基础已经过关、想往数据分析与可视化方向进阶的开发者。2. 先把技术栈和选型理由拆开语料来源、情感模型、数据模型分别怎么定2.1 股市情感语料从哪来四种常见来源怎么选抓新闻注意什么做股市情感分析第一步不是选模型而是先想清楚“情绪从哪句话里来”。这直接决定后面所有效果。对于课程设计和毕业设计最常见的四种语料来源是财经新闻的标题与正文首段。财经媒体和公司公告里“利好”“利空”“增长”“下滑”“中标”“减持”这类信号非常集中标题天然短、噪声少是情感分析成本最低的入口。股吧、财经社区评论。这类文本含金量高但伴随大量口水和反讽短文本里“呵呵”“就这”这类词极性不稳定需要额外做过滤和清洗适合作为加分项而不是主数据源。现成的中文情感语料数据集。分好类的商品评论或新闻标题数据集适合只想先把模型链路跑通、不想和爬虫纠缠的阶段也适合用同一份数据做论文复现对比。用历史行情数据自动构造弱标签。把“当日上涨超过 2% 的个股新闻”标成正面“下跌超过 2% 的标成负面”。这种弱标注不精确但在没有人工标注的情况下能帮你验证整套数据模型是否走得通。我一般建议把第 1 种和第 3 种组合前期用现成数据集验证模型中期写一个抓新闻脚本把财经新闻标题作为整个系统的在线输入。这样既保证了代码能跑通答辩时又有真实数据可讲。无论选哪种来源有三个参数必须在入库前处理发布时间统一成东八区时间文本统一去空白和 HTML 标签新闻 URL 或标题做 md5 去重。编码清一色用 UTF-8字符串里出现繁体、全角符号时先 normalize否则后续聚合时会出现同一家公司的新闻被拆成两行。2.2 情感模型怎么选词典法、SnowNLP 还是预训练模型很多初学者一上来就想用大模型但实际上课设和毕设的时间窗口内模型的“可解释性”比“准确率”更重要。情感分析在这里的价值是输出一条能画在行情图旁边的曲线不是发论文刷榜单所以选型要看运行速度、资源占用、能不能讲清楚原理。方案优点缺点适用场景jieba 分词 正负词典速度快、完全透明、每条结果能解释对否定句、反讽、程度词处理差演示情感原理、做基线SnowNLP中文友好、短文本稳定、pip 直接装通用语料训练财经词覆盖不足毕业设计首选FinBERT 类预训练模型金融语境准确度高依赖 torch、transformers运行慢有 GPU、追求效果SnowNLP 的sentiments接口返回 0 到 1 的浮点数越接近 1 代表越积极。作为默认模型它只需要 CPU 就能跑一个 500 条的新闻样本几秒钟出结果这是毕业设计现场演示时非常重要的优势。词典法不是不能用但要处理否定前缀。比如“公司净利润下滑”里“下滑”是负面词如果句子是“没有出现下滑”就得先扫描否定词再反转极性。我给出的默认做法是用 SnowNLP 做粗打分再用一个财经词典对特定词汇做加权修正这样程序可解释效果也够用。2.3 数据模型设计一条情感记录最少需要哪些字段数据模型是项目解析图里最容易被画成“数据库矩形”的部分但实际落地时可以很轻。我用 Python 的 dataclass 定义一条情感记录每条新闻抓回来之后先落成这个结构再进入聚合层。from dataclasses import dataclass, asdict from datetime import datetime dataclass class SentimentRecord: news_id: str # 新闻唯一ID由标题URL做md5生成用于去重 stock_code: str # 关联个股代码如 600519便于按股票筛选 published_at: datetime # 发布时间东八区决定它属于哪一天的情绪 title: str # 清洗后的新闻标题作为情感打分的主输入 content_snippet: str # 正文首段摘要保留用于人工核验 source: str # 来源标记news / forum / announcement raw_score: float # SnowNLP原始输出0~1保留以便做平滑 sentiment: str # 人工可读分类pos / neg / neu def to_row(self): return asdict(self)字段设计的逻辑是raw_score必须保留原始输出不要只存sentiment分类。因为后期画行情图时需要计算“当日平均情绪分”而平均分必须基于数值型原始分数如果提前把它转成 pos/neg 这种字符串后续聚合只能数个数损失太多信息。source字段很多人忽略。实际上新闻、评论、公告三类来源的情感分布差异很大比如公告通常中性偏正面股吧评论相对偏负面。如果你把它们混在一起直接求平均数据分析图上的情绪曲线会掩盖掉单一来源的信号。保留来源标签就能在聚合阶段按来源加权。3. 最小复现跑通从抓取新闻到产出当日情感分数需要哪些脚本3.1 环境准备Python 版本、依赖库和最小安装命令做这套项目我建议用 Python 3.9 以上版本主要原因是 pandas 和 mplfinance 在旧版 Python 上的类型兼容问题太多。如果你是照着 python 安装教程刚配好环境用 VSCode 写代码的话记得先在 VSCode 里把 Python 解释器指到虚拟环境否则后面pip install装到系统环境代码里 import 不到是最常见的第一课翻车。# 创建虚拟环境避免污染系统 Python python -m venv venv source venv/bin/activate # Windows 下用 venv\Scripts\activate # 安装最小依赖集合 pip install pandas numpy requests beautifulsoup4 snownlp mplfinance解释一下这些库的分工pandas 负责数据清洗和按日聚合requests 负责拿新闻页面beautifulsoup4 负责解析 HTMLsnownlp 负责情感打分mplfinance 负责画 K 线。这五个库就是整套项目的最小依赖。不需要一开始就装 torch 或 transformers等确定要上 FinBERT 再加不然光下载模型就能卡住半天。3.2 写一个新闻抓取脚本骨架requests、BeautifulSoup 与去重抓新闻是情感分析项目里最容易被低估的一步。很多同学写时只用requests.get()拿 HTML然后发现解析出来是空的——因为不少页面是动态渲染的数据标题不在初始 HTML 里而是由 JavaScript 从接口加载。我的做法是先确认目标页面的数据是静态还是动态。如果是静态页下面这个骨架就能用如果是动态页需要找到背后的 JSON 接口这个交给爬虫专项去处理这里先把基础版讲透。import re import time import requests from bs4 import BeautifulSoup from hashlib import md5 HEADERS { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) } def fetch_news_list(url: str, max_pages: int 3): items [] for page in range(1, max_pages 1): resp requests.get(url, headersHEADERS, timeout10) resp.encoding utf-8 # 中文财经站常见编码务必显式声明 soup BeautifulSoup(resp.text, html.parser) # 这里的 a.news-title 只是示例选择器实际要按目标站点的HTML结构调整 for node in soup.select(a.news-title): title node.get_text(stripTrue) href node.get(href) if href and title: items.append((title, href)) time.sleep(1) # 控制抓取频率避免对目标站造成压力 return items def clean_and_dedup(items): seen, out set(), [] for title, href in items: title re.sub(r\s, , title).strip() key md5((title href).encode(utf-8)).hexdigest() if key not in seen: seen.add(key) out.append({title: title, url: href}) return out参数说明里最值得关注的是timeout10和time.sleep(1)。前者防止某个页面迟迟不响应把整个脚本挂死后者是基本的抓取礼貌也是爬虫能持续跑下去的关键。实际使用中要根据目标站点的服务条款和 robots 协议调整频率这里的示例只作教学演示不鼓励高频率抓取。clean_and_dedup里的 md5 去重非常重要。新闻网站经常把同一篇稿子挂在多个栏目下或者分页时重复展示不去重的话后面的情感聚合会把同一条新闻计算两次情绪曲线出现虚假的尖峰。3.3 情感打分与聚合脚本把标题变成一条日级别情绪序列拿到清洗后的新闻标题列表下一步就是批量打分。这里我把之前定义的SentimentRecord用起来把每条标题变成记录对象然后按日期做聚合。import pandas as pd from snownlp import SnowNLP def score_title(title: str) - float: # SnowNLP.sentiments 返回 0~1越接近1代表情绪越积极 return SnowNLP(title).sentiments def build_daily_sentiment(records): # records: List[SentimentRecord]每条新闻一条记录 df pd.DataFrame([r.to_row() for r in records]) df[date] pd.to_datetime(df[published_at]).dt.date # 按日期分组计算当日情绪均值、标准差、新闻条数 daily df.groupby(date)[raw_score].agg( [mean, std, count] ) daily.columns [sentiment_mean, sentiment_std, news_count] return daily.reset_index()这里有两个参数值得展开mean是当日情绪均值std是当日情绪离散度。只看均值会丢掉分布信息比如某一天有 10 条新闻其中 9 条中性、1 条极度负面均值可能只有 0.44但单独看那 1 条可能才是真正的异动信号。所以我通常还会算一个quantile(0.1)作为“负面情绪爆发阈值”当它低于 0.3 时说明当天出现了明显利空信号。聚合到“日”级别有两个原因。第一A 股每天只有 4 小时交易时间分钟级情绪噪声太大日级别才能和 K 线对齐第二行情数据本来就以日为最小单位数据模型保持一致后续 merge 才不会出问题。4. 股市行情分析图怎么做K 线、成交量、情感曲线三合一4.1 用 mplfinance 画标准 K 线数据格式和必经的参数拿到行情数据后很多人第一反应是用 matplotlib 自己画蜡烛图但我更推荐 mplfinance。它是专门画金融时间序列的库对 OHLC 数据有内置校验画均线、成交量副图只需要传参数不用手动计算。行情数据这家先准备一个 CSV字段必须有date, open, high, low, close, volume其中 date 要转成 datetime 并设为索引否则 mplfinance 会直接报错。import pandas as pd import mplfinance as mpf # df 字段要求Open, High, Low, Close, Volume日期列转成索引 df pd.read_csv(stock_daily.csv, parse_dates[date], index_coldate) df.sort_index(inplaceTrue) # 取最近90个交易日typecandle 蜡烛图mav(5,20) 双均线 mpf.plot(df.tail(90), typecandle, mav(5, 20), volumeTrue, figsize(12, 6), styleyahoo)参数说明mav(5, 20)在蜡烛图上叠加 5 日和 20 日均线这是看短中期趋势最常用的组合volumeTrue会在主图下方生成一个成交量副图styleyahoo是内置配色红涨绿跌符合国内看图习惯。注意行情数据默认只有交易日周一至周五中可能夹着节假日。如果你直接用 pandas 原始索引画图K 线之间会出现缺口视觉上会有奇怪的“空洞”。mplfinance 默认按索引连续绘制所以必须在数据处理阶段就把停牌日和节假日剔除或者在画图时用show_nontradingFalse显式指定。4.2 把情感折线叠到 K 线图上twinx 双坐标轴与日期对齐现在入场重头戏把第 3 章算出的daily情感序列画到 K 线图上方。这里最关键的技巧是给主图坐标轴创建一个共享 x 轴的孪生坐标轴让情感线覆盖在蜡烛图之上但 y 轴刻度独立在右侧。fig, axes mpf.plot(df.tail(90), typecandle, mav(5, 20), volumeTrue, figsize(12, 6), styleyahoo, returnfigTrue) # axes[0] 是主图区域twinx 创建一个共享 x 轴的右侧 y 轴 ax axes[0].twinx() ax.plot(daily[date], daily[sentiment_mean], color#FF5722, linewidth1.5, labelsentiment) ax.axhline(0.5, colorgray, linewidth0.8, linestyle--) ax.set_ylabel(sentiment, fontsize10) ax.legend(locupper left, fontsize9)代码里returnfigTrue是必须的否则 mplfinance 直接渲染并关闭 figure你拿不到 axes 对象。twinx()创建的第二个 y 轴和主图共用 x 轴这样情感线就叠在 K 线上方不会被挤到副图去。那根灰虚线是 0.5 的中性分界线一眼就能看出哪天情绪偏暖、哪天偏冷。这里最常见的错误是daily[date]的类型和df.index对不上。K 线索引是Timestampdaily 里的 date 是datetime.date直接 plot 时 x 轴会变成两段后半段全是空白。解决办法是先把两边统一成字符串做 inner join再用结果画图。4.3 验证情感是否“先行”用 shift 计算滞后相关性股市行情分析图做出来之后总会有人问“这曲线到底有没有用”。最好的回应不是嘴上解释而是算一个简单相关性。逻辑是如果 T 日新闻情绪真的领先价格那么 T 日情绪分和 T1 日收益率应该呈现正相关。# 先把情感序列和收盘价按日期对齐 daily[close] df[Close].groupby(df.index.date).last().values daily[ret] daily[close].pct_change() # 分别测试 T 日情绪与 T1 / T2 / T3 日收益率的相关系数 for lag in [1, 2, 3]: corr daily[sentiment_mean].corr(daily[ret].shift(-lag)) print(fT日情绪 vs T{lag}日收益率: {corr:.3f})shift(-lag)表示把收益率向上移动 lag 天让第 T 天的情绪和第 Tlag 天的收益对齐。相关系数绝对值超过 0.1 就算有微弱信号超过 0.3 在日频金融数据里已经非常罕见。如果三个滞后期的相关系数都接近 0不要急着否定项目。财经新闻对股价的影响本来就短而且很多重大利好在新闻发布前已经被市场预期消化。这时图表呈现的“情绪与价格同涨同跌”本身就是有效结论可以作为“当日情绪同步验证”写进项目文档。5. 股市情感分析避坑最容易让项目翻车的 5 个细节5.1 情感分数全部挤在 0.45 到 0.52曲线像一条“死线”现象画出来的情绪曲线几乎是一条直线新闻再多也波动不起来。原因SnowNLP 是在通用中文语料上训练的对财经词汇的敏感度很低标题里真正带强烈情感色彩的词不多大量新闻都是中性陈述比如“某公司发布年度报告”。解决加一层财经词典加权让命中财经关键词的新闻偏离 0.5 中性线。做法是先定义两个集合正负面词各一二十个然后在 SnowNLP 原始打分基础上做加权修正FIN_POS {增长, 盈利, 中标, 回购, 创新高, 超预期} FIN_NEG {亏损, 下滑, 减持, 处罚, 退市, 低于预期} def fin_score(title: str) - float: base SnowNLP(title).sentiments for w in FIN_POS: if w in title: base 0.7 * base 0.3 * 0.85 # 向 0.85 方向拉 for w in FIN_NEG: if w in title: base 0.7 * base 0.3 * 0.15 # 向 0.15 方向拉 return max(0.0, min(1.0, base))加权公式要义是“向目标方向拉 30%”而不是直接改成一个固定值这样保留了原始模型对中性句的判断曲线既敏感又不过度突变。5.2 新闻发布时间错乱昨晚的新闻算进了今天现象情绪突变总是比行情反转晚一天图形上看是情绪线“滞后”于 K 线。原因很多新闻网站展示的发布时间是“更新时间”或“本地化后今天”而不是真实发布时刻。晚上 11 点的新闻被页面标成当天日期入库后就被并进了当天情绪导致你看到的是“情绪昨天很热今天行情下跌”的错误错位。解决统一时间规范全部转成东八区并明确“收盘后新闻归下一个交易日”。A 股 15:00 收盘收盘后发布的公告和新闻对当日行情已经没有影响力应该归到下一个交易日再参与情绪聚合。写一个归一化函数def assign_trade_date(published_at: pd.Timestamp) - pd.Timestamp.date: t published_at.tz_convert(Asia/Shanghai) if published_at.tzinfo else published_at if t.hour 15: # 收盘后顺延到下一个交易日 return t.normalize() pd.Timedelta(days1) return t.normalize()这个函数管住了“情绪属于哪一天”的边界是整套项目里最容易忽视却最影响结论正确性的函数之一。5.3 情绪均值总偏正面连跌三天还是一片红现象某只股票连跌一周情绪均值却始终在 0.55 以上。原因新闻源本身有偏向。财经媒体更爱报道“签约”“投产”“评级买入”这类正面事件负面信息往往分散在深度报道里标题占比低同时公告文本普遍措辞中性偏正面把均值拉高了。解决不要只算全局均值按source字段分组计算 z-score再按来源加权合成。比如公告类权重 0.3新闻类 0.5评论类 0.2。这样即使某一来源整体偏正面也不会污染整个情绪曲线同时把quantile(0.1)作为负面信号指标比均值更能捕捉少见的利空。5.4 K 线索引和情感日期对不上图右边出现大片空白现象情感线在 K 线图上只覆盖中间一段两边不是缺头就是缺尾。原因K 线数据只有交易日情感数据按自然日聚合两者长度天然不一致直接用行号画图必然错位。解决画图前先做一次日期对齐统一用字符串日期作为连接键daily[date_str] pd.to_datetime(daily[date]).dt.strftime(%Y-%m-%d) df[date_str] df.index.strftime(%Y-%m-%d) merged df.reset_index().merge(daily, ondate_str, howinner) merged.set_index(date, inplaceTrue)howinner确保只保留两边都有的日期。代价是节假日那几天没有情感点但这是正确做法宁可少几个点也不要错位画图。5.5 数据量太少情绪曲线抖动剧烈答辩时说不清规律现象总共只有几百条新闻分到 60 个交易日平均一天不到 10 条画出来的折线像锯齿均值没有参考性。原因语料来源太窄只抓了一个网站或者过滤条件过于严格把大量新闻误删了。解决扩展抓取范围到 2 到 3 个财经源同时把统计周期从日改为滚动窗口。用 3 日移动平均平滑曲线并保留原始 std 区间作为阴影带daily[smoothed] daily[sentiment_mean].rolling(3, min_periods1).mean() ax.plot(daily[date], daily[smoothed], linewidth2) ax.fill_between(daily[date], daily[smoothed] - daily[sentiment_std], daily[smoothed] daily[sentiment_std], alpha0.2, color#FF5722)min_periods1让最前面几天也能画出平滑值避免前两天空白。阴影带展示了情绪的不确定性这段代码几乎不加成本却能让行情分析图看起来专业一个档次。6. 拿去答辩和演示前把这三个细节改掉6.1 抓取和打分分离数据落中间文件答辩现场最怕的是断网。把抓新闻和情感打分写成两个脚本中间结果落到 SQLite 或 CSV演示时直接读缓存程序照常跑通。这样既展示了数据模型也规避了现场网络波动。import sqlite3 import pandas as pd def load_or_fetch(symbol: str, force: bool False): conn sqlite3.connect(sentiment_cache.db) df pd.read_sql(SELECT * FROM news WHERE symbol ?, conn, params[symbol]) if df.empty or force: df fetch_and_score(symbol) # 内部完成抓取打分 df.to_sql(news, conn, if_existsappend, indexFalse) conn.close() return df这份缓存代码的意义在于让整个项目可以反复演示同一组结果不影响二次运行。至少留存 60 个交易日的新闻数据能画出一个月以上的历史情绪曲线可信度比 10 天的片段高很多。6.2 在图上标注情绪突变点让别人一眼看到决策逻辑答辩时评委不会盯着平均情绪线看他们会问“这条曲线有什么用”。最好的回答是提前在图上标注几个明显的情绪突变点。做法很简单在sentiment_mean的相邻差值超过阈值的日期打上注解。jump_dates merged.loc[merged[sentiment_mean].diff().abs() 0.15] for d, row in jump_dates.iterrows(): ax.annotate(sentiment jump, xy(d, row[sentiment_mean]), xytext(15, 5), textcoordsoffset points, fontsize8, colorblack)选阈值时注意先看自己数据的波动范围如果情绪值集中在 0.4 到 0.6阈值 0.15 就合适如果数据特别平缓可以降到 0.1。6.3 输出 PNG 而不是交互窗口演示现场最怕窗口闪烁或缩放变形。在脚本开头固定 matplotlib 的渲染后端直接保存高分辨率 PNG图片上注释写清楚参数比现场放大交互窗口稳妥得多import matplotlib matplotlib.use(Agg) import matplotlib.pyplot as plt # 在 plt.savefig 前调用 plt.savefig(sentiment_analysis.png, dpi150, bbox_inchestight)dpi150在屏幕投影上足够清晰bbox_inchestight避免图表边缘被截断。保存 PNG 的同时保留生成脚本让评审可以现场改参数重跑。做这类项目我习惯把“发布时间的收盘边界判断”放在所有代码最前面先跑一遍很多所谓模型不准的结论最后都发现是时间标签错位造成的。先把时间对齐再把锅丢给模型这是血泪经验。希望帮到你。本文还有配套的精品资源点击获取
返回列表