简介:一款基于网络舆情的Python股票分析系统源码与说明文档,属于高分大作业,评审得分九十八分。系统整合了网络爬虫、自然语言处理与股票数据接口,完整覆盖舆情数据采集、情感分析、可视化展示及预测模型等核心环节,面向计算机专业学生与Python数据分析学习者,难度适中,适合课程设计、毕业设计或量化入门实战。压缩包共89个文件,约2.87MB,主要包含49个Python源文件、11个CSV数据文件、9个HTML页面模板、3个Word说明文档,以及SQL数据库脚本、环境配置文件等,目录结构清晰,便于按功能模块阅读与二次开发。包内提供完整项目代码、项目立项/计划/需求分析文档、数据库及运行配置,已有88人学习浏览。通过该项目可以系统了解舆情数据如何抓取与清洗、文本情感如何量化、股票价格如何关联分析,并尝试构建简单预测模型,是理论与实践结合的参考样例。
1. 基于网络舆情的Python股票分析系统:为什么多数同类大作业只能拿到及格分
「基于网络舆情的 Python 股票分析系统」这个题目,最常见的翻车方式是把系统做成两个互不相干的 demo:一边用爬虫抓新闻再跑个情感分析给评论打正负分,另一边用现成库拉行情画 K 线,最后用两张漂亮的图强行凑成一个「系统」。但答辩时导师只要追问一句「舆情分数是怎么变成交易信号的」,整个项目就露馅了。这类系统真正该交付的是一条证据链:从舆情采集、文本清洗、情感打分,到行情对齐、时序验证,每一步都要能回答「为什么这个分数能解释或预测股价变化」。这篇文章按一条可落地的链路拆开讲,适合正在写课程设计或毕业设计、手里已经堆了代码但拼不拢的开发者。你会发现,难点不在情感模型选什么,而在数据对齐和未来函数这两个隐蔽在表格里的坑。
2. 舆情数据层怎么搭:定向爬虫的取舍、清洗规则与评论情绪字段设计
2.1 先定舆情源:为什么选财经新闻加股吧评论,而不是全量微博
舆情源的选型直接决定后续清洗成本。常见的可选项有财经新闻站、股票论坛、雪球、微博话题。我一般只建议取前两种:「财经新闻」负责公告、政策、行业层面的信息增量,特点是文本规范、时间戳可靠、几乎没有表情符号;「股票论坛短评」负责散户情绪与题材炒作,噪声大,但恰恰是「舆情」最真实的部分。微博话题不是不行,而是反爬成本高、登录墙多、水军比例大,把课程设计的时间花在清洗微博数据上不值得。另一个容易被忽略的原则是「样本量比覆盖面重要」:稳定抓取两个信源、覆盖目标股票最近三到六个月的历史文本,好过抓十个源却断断续续、字段对不齐。
信源选定后要做一次可用性验证,而不是直接写全量爬虫。先用浏览器打开目标列表页,确认以下四件事:一是列表页返回的是静态 HTML 还是动态渲染(动态渲染意味着要上 Selenium,成本陡增);二是发布时间是否完整显示;三是是否分页且 URL 规律;四是直接 requests 请求是否返回 403。四件事里任意一件不满足,就换备选源,别硬顶。课程设计里的爬虫代码只是数据管道的前置环节,不值得为它造轮子。
2.2 采集脚本骨架:requests 加 lxml,按列表页倒序抓取
确定信源后,采集粒度建议是「标题 + 摘要 + 发布时间」,不抓正文全文。抓正文会带来两类问题:一是正文里大量与个股无关的页面噪声,二是同一新闻在不同版面转载时正文可能微调,导致去重失效。下面是一个可直接改的列表页采集骨架,URL 换成你验证过的信源即可。
import time import sqlite3 import requests from lxml import etree UA = ("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " "(KHTML, like Gecko) Chrome/124.0 Safari/537.36") def fetch_list(url: str, timeout: int = 5, retry: int = 2) -> str | None: """抓取列表页 HTML,失败时按指数退避重试。""" for attempt in range(retry + 1): try: resp = requests.get(url, headers={"User-Agent": UA}, timeout=timeout) resp.raise_for_status() resp.encoding = resp.apparent_encoding return resp.text except (requests.RequestException, ValueError): if attempt < retry: time.sleep(1.5 * (attempt + 1)) time.sleep(0.8) return None def parse_news_items(html: str) -> list[dict]: """从列表页抽取标题与发布时间。 下面的 XPath 需要按目标站实际结构调整, 建议挑选 class 命名稳定的列表项节点。""" tree = etree.HTML(html) out = [] for item in tree.xpath("//div[contains(@class,'list')]//div[contains(@class,'item')]"): title = item.xpath(".//a[@title]/text()") post_time = item.xpath(".//span[contains(@class,'time')]/text()") if not title: continue out.append({ "title": title[0].strip(), "post_time": post_time[0].strip() if post_time else "", }) return out逻辑说明:fetch_list里把超时设为 5 秒、重试 2 次,是因为列表页通常不大,5 秒没返回基本可以判定超时;resp.encoding = resp.apparent_encoding是为了避免中文乱码,requests 有时根据响应头推断的编码和页面实际编码不一致。parse_news_items的 XPath 只是示例,实际使用时先在浏览器开发者工具里确认节点结构再改。参数上,单线程循环加time.sleep(0.8)足够应付课程设计的数据量,不建议上多线程或异步,稳定比速度重要。
2.3 入库与去重:SQLite 单表,MD5 判重加前缀相似度兜底
抓下来的数据落在 SQLite 单表里最省事,不需要额外安装数据库服务。建表时把「标题加发布时间」的哈希值设为唯一键,能挡住完全重复的转载;但同一条新闻换个标题模板重发的情况,哈希判重拦不住,需要再加一层基于前缀的相似度去重。
import difflib import hashlib def init_db(path="sentiment.db"): conn = sqlite3.connect(path) conn.execute(""" CREATE TABLE IF NOT EXISTS news ( id INTEGER PRIMARY KEY AUTOINCREMENT, symbol TEXT, source TEXT, title TEXT, content TEXT, post_time TEXT, url TEXT, content_hash TEXT UNIQUE ) """) return conn def is_similar(title: str, seen_titles: list[str], prefix_len: int = 40, threshold: float = 0.88) -> bool: a = title[:prefix_len] for old in seen_titles: b = old[:prefix_len] if difflib.SequenceMatcher(None, a, b).ratio() >= threshold: return True return False def insert_news(conn, items: list[dict], symbol: str, source: str): cur = conn.cursor() seen: list[str] = [] saved = skipped = 0 for it in items: t = it["title"] if is_similar(t, seen): skipped += 1 continue seen.append(t) content_hash = hashlib.md5( (t + it["post_time"]).encode("utf-8")).hexdigest() cur.execute( "INSERT OR IGNORE INTO news(symbol, source, title, content, " "post_time, url, content_hash) VALUES(?, ?, ?, ?, ?, ?, ?)", (symbol, source, t, it.get("content", ""), it["post_time"], it.get("url", ""), content_hash)) saved += 1 conn.commit() print(f"[{symbol}] 新增 {saved} 条,相似度去重 {skipped} 条")逻辑说明:is_similar只比较标题前 40 个字符,是因为新闻标题前半段通常包含股票代码或公司名,信息密度最高;difflib.SequenceMatcher的 ratio 在 0.88 时能拦掉「换个标点、加个前缀」的转载,同时保留正常标题的区分度。INSERT OR IGNORE配合唯一键content_hash,防止同一批数据重复执行时产生脏数据。一个实用习惯是每次抓完打印「新增 / 去重」数量,如果连续多次新增数都为零,优先怀疑信源结构调整或反爬拦截。
3. 情感分析:让模型读得懂「利好出尽」这类反讽
3.1 为什么不用现成接口,而是自建金融词典打分
很多同学拿到情感分析任务,第一反应是调一个现成的自然语言处理接口,传一段文本就返回情感分数。这类方案在股票舆情上有两个致命问题:一是通用模型没有见过「利好出尽」「业绩爆雷」「高开低走」这类财经表达,判断经常出错;二是结果不可解释,答辩时导师问「这个 -0.3 是怎么算出来的」,你没法把计算过程画出来。自建打分函数的好处是每一步都可追溯:这句得分是「利好」加 1 分、被「出尽」反转成 -1 分,逻辑写在代码里,答辩时直接把函数打印出来讲即可。
常见的补充方案是拿 snownlp 或类似轻量库做兜底对比,但它内部的朴素贝叶斯语料偏购物评论,对「回调太深」「主力出货」这类表达的处理不稳定。课程设计里,可解释性通常比模型精度更值钱,所以我建议主引擎用自定义词典,兜底才考虑通用库。词典不用追求大而全,覆盖目标股票相关新闻和评论里的高频词就好,先维护正向、负向、程度副词、反转词四张词表。
| 词表类型 | 示例词 | 作用 |
|---|---|---|
| 正向表 | 利好、大涨、涨停、突破、回购、增持 | 累加分数 |
| 负向表 | 利空、大跌、跌停、减持、爆雷、亏损 | 累减分数 |
| 程度副词表 | 非常 1.5、大幅 1.4、极度 1.8 | 放大相邻极性词得分 |
| 反转词表 | 出尽、低于预期、难以持续 | 翻转窗口内极性词的符号 |
3.2 打分函数:分词、词典匹配与反转窗口纠偏
有了四张词表,打分逻辑就是「分词、查表、按窗口纠偏」。下面这段代码是一个可运行的骨架,分词用 jieba,反转窗口设为当前词往前 2 个词。
import re import jieba POS_WORDS = {"利好", "大涨", "涨停", "突破", "回购", "增持", "超预期"} NEG_WORDS = {"利空", "大跌", "跌停", "减持", "质押", "爆雷", "亏损", "不及预期"} BOOSTERS = {"非常": 1.5, "大幅": 1.4, "极度": 1.8, "明显": 1.3, "超": 1.2} REVERSAL_WORDS = {"出尽", "低于预期", "难以持续"} def clean_text(text: str) -> str: # 去掉 emoji 与多余符号,只保留中文、英文、数字和常用标点 return re.sub(r"[^\u4e00-\u9fa5A-Za-z0-9,。!?、:%]", " ", text) def sentiment_score(text: str) -> float: tokens = list(jieba.cut(clean_text(text))) score = 0.0 boost = 1.0 n = len(tokens) i = 0 while i < n: w = tokens[i] if w in BOOSTERS: boost = BOOSTERS[w] elif w in POS_WORDS: score += boost boost = 1.0 elif w in NEG_WORDS: score -= boost boost = 1.0 if w in REVERSAL_WORDS: # 反转词只作用于前 2 个词窗口内的极性词 for j in range(max(0, i - 2), i): if tokens[j] in POS_WORDS: score -= 1.0 elif tokens[j] in NEG_WORDS: score += 1.0 i += 1 # 截断到 [-5, 5],防止单条极端长文主导聚合指标 return round(max(-5.0, min(5.0, score)), 3)逻辑说明:boost变量保存最近一次出现的程度副词,并作用到下一个极性词上,所以「非常看好」会比「看好」多 0.5 分;每次极性词消费完boost后重置为 1.0,避免连续程度副词累乘导致分数爆炸。反转逻辑里score -= 1.0而不是减boost,是因为「利好出尽」这种短语里,反转对象的权重不该被程度副词二次放大。分数线截断到 [-5, 5] 是工程上的取舍,防止某条帖子刷屏式表达把当天的聚合结果带偏。实际使用时,可以把股票相关术语加进 jieba 词典,比如jieba.add_word("利好出尽"),但这属于优化项,不影响整体架构。参数上的两个经验值是窗口取 2、阈值取 5,如果你想更激进,窗口扩到 3 能捕获「利好……出尽」这类跨词反转,代价是误反转率上升。
3.3 把单条分数聚合成日级舆情特征:关注度、分歧度与多空一致性
单条文本的情感分数只是中间产物,系统真正消费的是「某只股票在某个交易日的舆情状态」。聚合时不要只求平均分,至少保留四个维度:情绪总分、情绪均值、情绪方差、看多比例。总分代表关注度,方差代表分歧度,看多比例代表一致性,这四类信息在后续与行情对齐时各有用途。
import pandas as pd def daily_emotion(df: pd.DataFrame) -> pd.DataFrame: df = df.copy() df["date"] = pd.to_datetime(df["post_time"]).dt.date grouped = df.groupby(["symbol", "date"]).agg( emotion_sum=("sentiment", "sum"), emotion_mean=("sentiment", "mean"), emotion_var=("sentiment", "var"), comment_count=("sentiment", "count"), bull_ratio=("sentiment", lambda s: (s > 0).mean()), ).reset_index() grouped["emotion_var"] = grouped["emotion_var"].fillna(0) return grouped逻辑说明:emotion_sum能反映舆情热度,某天突然放量讨论通常对应题材发酵或利空落地;emotion_var在样本量只有 1 条时会得到 NaN,用 fillna(0) 把它解释为「无分歧」更合理;bull_ratio是正向评论占全部评论的比例,它比emotion_mean更稳健,因为平均分容易被一条极端高分长文带偏。聚合后如果发现某只股票大量交易日comment_count为零,说明信源覆盖不足,这会在下一章的对齐环节暴露出问题。
聚合函数跑通后,先人工抽样验证:打印几条高分和低分评论,用肉眼确认评分方向正确。这一步不能省,词典方法最大的风险是词表覆盖不全导致系统性偏差,而抽样能最快暴露问题。我的习惯是每只股票抽 20 条正分和 20 条负分逐条读,别嫌麻烦,情感分析的坑基本都靠这一步排掉。
4. 行情数据接入与特征对齐:把舆情表和 K 线拼成一张能分析的表
4.1 行情接口选型:免 token 的日线接口更适合课程设计
舆情数据准备好后,需要拉取目标股票的历史行情。常见做法是使用现成的财经数据接口;新版接口大多需要注册 token 并积分,课程设计场景直接用免 token 的开源行情接口更省事,至少能快速拿到前复权日线。拿到数据后的第一步是统一列名,接口返回的往往是中文列名,而后续特征工程里全部要用英文列名。
import akshare as ak import pandas as pd def load_kline(symbol: str, start: str = "20240101", end: str = "20240630") -> pd.DataFrame: df = ak.stock_zh_a_hist( symbol=symbol, period="daily", start_date=start, end_date=end, adjust="qfq", # 前复权,适合做收益率计算 ) df.rename(columns={ "日期": "date", "开盘": "open", "收盘": "close", "最高": "high", "最低": "low", "成交量": "volume", }, inplace=True) df["date"] = pd.to_datetime(df["date"]) df["ret"] = df["close"].pct_change() return df[["date", "open", "close", "high", "low", "volume", "ret"]]逻辑说明:period="daily"限制在日线,课程设计不需要分钟线;adjust="qfq"表示前复权,这样算出来的涨跌幅不会被除权除息跳空污染。接口返回的列名在不同版本里可能微调,如果rename报 KeyError,先打印df.columns看实际列名,不必硬猜。另一个常见问题是接口偶尔变更导致拉数失败,这属于外部依赖的不可控因素,稳妥的做法是把拉到的行情存一份本地 CSV 快照,后续反复读取本地文件而不是每次重新请求,既快又能防止答辩现场断网。
4.2 舆情与行情合并:以交易日为主表,情绪缺失填 0 并打标记
合并表是整个系统的核心结构,绝大多数后续分析和可视化都跑在这张表上。合并的原则是「以行情交易日为主表,舆情副表左连接」,而不是 inner join。 inner join 会丢掉舆情缺失的交易日,而这些日子里股价可能大跌,丢掉它们会让回测结果看起来异常美好,属于典型的幸存者偏差。
def merge_market_news(market: pd.DataFrame, news: pd.DataFrame) -> pd.DataFrame: market = market.copy() news = news.copy() market["date"] = pd.to_datetime(market["date"]).dt.date news["date"] = pd.to_datetime(news["date"]).dt.date merged = market.merge( news, on=["date"], how="left", suffixes=("", "_news")) for col in ["emotion_sum", "emotion_mean", "emotion_var", "comment_count", "bull_ratio"]: if col in merged.columns: merged[col] = merged[col].fillna(0) merged["has_news"] = (merged["comment_count"] > 0).astype(int) return merged逻辑说明:how="left"保证行情表的每个交易日都保留,舆情缺失的日期情绪字段被填 0。这里有一个关键区分:has_news=0表示「当日没有舆情文本」,它与「有舆情但情绪中性」是两回事,后续建模时可以把has_news当作一个开关特征;如果不加这个标记,模型会把「没消息」和「中性消息」混为一谈。suffixes参数用来处理两张表可能同名字段,尤其是source、url这类列。合并后检查一下行数是否与行情表一致,如果多出几行,通常是舆情表里出现了行情表之外的日期,比如周末或节假日,需要回到采集端检查发布时间清洗。
4.3 先算同步相关与滞后相关:同步相关是行情回声,滞后才是预测信号
合并表刚生成时,很多人会迫不及待地做相关性分析,最常见的一步是算「当日舆情平均分」与「当日涨跌幅」的相关系数,算出来还挺高,于是得出结论说舆情能预测股价。这个结论是错的:当日舆情大多在盘中或盘后产生,与当日涨跌幅处于同一时间窗口,无论谁因谁果,它都不能作为交易信号。真正的问题是「今天的舆情能否解释明天的涨跌」,也就是滞后相关。
def check_lead_lag(m: pd.DataFrame) -> tuple[float, float]: m = m.sort_values("date").reset_index(drop=True) m["next_ret"] = m["ret"].shift(-1) m = m.dropna(subset=["next_ret"]) sync_corr = m["emotion_mean"].corr(m["ret"]) lead_corr = m["emotion_mean"].corr(m["next_ret"]) print(f"同步相关: {sync_corr:.3f},滞后相关: {lead_corr:.3f}") return sync_corr, lead_corr逻辑说明:shift(-1)把后一天的收益率搬到今天这一行,相当于构造「今天的情绪 vs 明天的收益」。注意dropna(subset=["next_ret"])是为了删掉最后一行没有未来收益的样本。这里想传达一个判断方法:如果同步相关明显高于滞后相关,说明舆情更多是行情的回声而不是先行指标;如果滞后相关显著且方向合理,才值得继续做事件研究或特征构造。这个检验是整个系统里最容易被忽视但又最能体现「系统感」的一步,答辩时主动摆出这两组数字,比贴十张预测曲线更有说服力。
「先算相关、再决定要不要建模」的顺序也能帮你避开另一个坑:如果你跳过相关性检验直接上线性回归或随机森林,模型可能学到「舆情温度计」而不是「舆情预测器」,训练集分数不错但验证集上一塌糊涂。数据对齐里的未来函数问题,在下一章详细排查。
5. 舆情股票分析系统的五个必踩坑:从数据错位到词典失效
5.1 热门股当天刷屏,情绪被少数重复文本绑架
现象:某天某只股票的情绪均值突然冲到 1.5 以上,拉出明细一看,300 条文本里有 280 条是同一篇文章的转载或同一个人连发的重复短评,情绪指标被少数文本绑架,后续与行情对齐产生假信号。
原因:股票论坛和新闻聚合站都存在转载机制,同一篇公告会被多个账号分时段重发;热门股在话题发酵时,同一用户连续发帖的情况也很常见。
解决:在入库阶段增加「前缀相似度」去重,并对同一只股票在同一自然日内的有效文本数做上限控制。前缀去重代码可以用 difflib 实现,前面第 2 章的is_similar函数已经做了基础版本;更严格的做法是同一用户 10 分钟内发布的相似内容只保留一条。
5.2 未来函数:用当日舆情「预测」当日涨跌的幻觉
现象:同步相关系数计算出来高达 0.5,滞后相关却只有 0.02,于是拿着 0.5 的结论在报告里声称找到了预测信号。
原因:舆情文本自带时间戳,但很多爬虫按「抓取时间」而不是「发布时间」入库。盘中抓到的帖子被标成抓取时间,当天收盘后做对齐时,这些盘中评论被错误地当成「当天收盘后才知道的信息」,和当日 K 线形成了因果倒置。
解决:入库时严格保存发布时间,并要把 15:00 收盘之后以及次日开盘前的舆情归到下一个交易日。关键代码如下:
def shift_to_trade_day(d: pd.Timestamp, trade_cal: set) -> pd.Timestamp: while d.date() not in trade_cal: d += pd.Timedelta(days=1) return d def assign_trade_date(df: pd.DataFrame, trade_cal: set) -> pd.DataFrame: df = df.copy() df["dt"] = pd.to_datetime(df["post_time"]) after_market = (df["dt"].dt.hour > 15) | ( (df["dt"].dt.hour == 15)) before_open = (df["dt"].dt.hour < 9) | ( (df["dt"].dt.hour == 9) & (df["dt"].dt.minute < 30)) df["trade_date"] = df["dt"].dt.date df.loc[after_market | before_open, "trade_date"] = pd.NaT df["dt"] = df["dt"].where(~(after_market | before_open)) # 重新映射到交易日 dates = df["trade_date"].astype(object) for i, row in df.iterrows(): if pd.isna(row["trade_date"]): shifted = shift_to_trade_day(row["dt"], trade_cal) df.at[i, "trade_date"] = shifted.date() return df这段代码的核心规则是:15:00 之后发布的舆情,真正可被交易利用的最早时间是下一交易日,所以必须归到下一个交易日;次日 9:30 之前的早间新闻同理。trade_cal可以来自行情接口的交易日历表。这一步做完,再重新跑check_lead_lag,同步相关会明显下降,滞后相关才可能露出真实信号。
5.3 停牌、一字板与零成交量行:合并表里的隐形脏数据
现象:合并表某些行volume为 0、close连续多日不变,把它们纳入相关性计算后,整体相关系数被稀释或扭曲。更隐蔽的情况是停牌复牌首日ret用停牌前收盘价计算,导致复牌日的涨跌幅失真。
原因:行情接口对停牌日有两种处理方式,一是直接缺行,二是补一行零成交占位。两种方式与舆情表合并时都会产生语义不明的行,影响后续所有统计。
解决:合并前先过滤掉零成交占位行,复牌首日收益率使用复牌日收盘价相对停牌前收盘价重新计算。实际操作里,open为 0 或volume为 0 的行直接删除即可;对复牌首日的涨跌幅,不要用pct_change()的默认行为,而是显式计算:
def fix_resume_ret(df: pd.DataFrame) -> pd.DataFrame: df = df.sort_values("date").reset_index(drop=True) # 查找前收盘价为 NaN 或与当日开盘差异过大的行 mask = (df["volume"] == 0) | (df["open"] == 0) df = df[~mask].reset_index(drop=True) df["ret"] = df["close"] / df["close"].shift(1) - 1 return df参数说明:shift(1)取前一行收盘价,如果前面已经删掉了停牌占位行,那前一行就是有效交易日,计算出的复牌首日涨跌幅自然正确。课程设计里不需要处理极端复杂的除权场景,前复权数据加shift(1)足够。
5.4 中文情感词典对「利好出尽」失效
现象:「涨停第二天就跳水,利好出尽,明天等着吃面」这句话里同时出现「涨停」「利好」和「出尽」,基础词典逻辑会把它算成正分,而实际上这句话的情绪是负向的。
原因:词典打分只看词的极性,没有识别短语结构里的反转关系。「利好出尽」是一个整体,它表示「利好已经被消化完毕」。
解决:在打分函数里加入反转窗口逻辑,前面 3.2 小节的REVERSAL_WORDS已经处理了「出尽」这类词,但要注意一个缺陷——反转窗口不加限制时,「利好」出现在反转词 5 个词以外也被翻转,可能误伤正常句子。我的建议是窗口只取前 3 个词,并且一条评论里同一个反转词只触发一次,避免多个反转词叠加导致分数来回震荡。
5.5 反爬返回 200 状态码但解析结果为空:信源静默失效
现象:爬虫日志显示每天都在跑,但入库条数连续三天为 0;抓下来的 HTML 里根本没有预期节点,而是验证码页或安全提示页。
原因:目标站点加了访问频率校验或需要带特定 Cookie;requests 拿到的是 200 响应,但响应体内容不是列表页本身,所以 XPath 解析不到任何条目。
解决:在解析前先做一次「响应内容体检」,把验证码关键词的判断前置。
def looks_like_captcha(html: str) -> bool: for kw in ("验证码", "安全验证", "captcha", "访问过于频繁"): if kw in html: return True return False def fetch_with_check(url: str) -> str | None: html = fetch_list(url) if html and looks_like_captcha(html): print(f"[warning] 疑似验证码页: {url}") return None return html如果连续触发反爬,不要硬刚,更不要尝试高并发多线程重试;课程设计的方向是数据自洽,不是爬虫对抗。最实用的兜底方案是维护一份本地 CSV 快照,把已经抓到的原始数据导出存档,后续分析全部基于快照进行。答辩现场优先演示本地分析流程,爬虫作为数据更新链路动态展示,这样即使某个信源临时封禁,系统核心功能也不会瘫掉。
6. 事件研究法验证舆情信号:一个让答辩翻盘的进阶技巧
当同步相关与滞后相关的对比结果合理后,下一步不要急着上机器学习模型,先做一个事件研究:把每日舆情得分从高到低排序,取前 10% 的日子作为「舆情事件日」,统计事件日之后 1、3、5 个交易日的平均累计收益。这个方法直观、可解释、且不容易被数据泄漏污染。
import numpy as np def event_study(df: pd.DataFrame, top_ratio: float = 0.1, horizons=(1, 3, 5)) -> pd.Series: df = df.dropna(subset=["ret"]).reset_index(drop=True) top = df["emotion_mean"].quantile(1 - top_ratio) events = df.index[df["emotion_mean"] >= top].tolist() out = {} for h in horizons: cumrets = [] for e in events: # 事件日当天收盘后舆情才确定,所以从 e+1 开始取收益 seg = df.iloc[e + 1: e + h + 1] if len(seg) == h: cumrets.append((1 + seg["ret"]).prod() - 1) out[f"事件后{h}日累计收益"] = float(np.mean(cumrets)) return pd.Series(out)逻辑说明:df.iloc[e + 1: e + h + 1]刻意跳过事件日当天的收益,因为事件日舆情是用当天行情信息确认的,计入当天收益就犯了未来函数的老毛病;(1 + seg["ret"]).prod() - 1是把区间内每日收益率复合成累计收益。更严谨的做法是再取一个对照组——随机抽取同样数量且非事件日的起点,计算同期平均收益,用事件组减去对照组得到「超额收益」。如果事件后 3 日累计收益显著高于对照组,这份舆情特征才算真正通过了初步验证。
三个收尾习惯值得养成:第一,每次改动数据管道后,先重跑同步相关与滞后相关对比,如果同步相关仍然远高于滞后相关,优先怀疑日期对齐逻辑;第二,随手存一份本地 CSV 快照,答辩现场即使断网也不影响演示核心链路;第三,把自定义情感词典抽成独立的 json 文件放在项目目录,替换词表时不需要改动任何代码。我的习惯是每次改完清洗逻辑,先把情绪列随机打乱再跑一遍相关检验——打乱后相关性应该归零,如果没归零,说明代码里藏着跟真实日期有关的泄漏路径。这个大作业方向的本质是数据工程与验证逻辑的完整性,把这条链路走通,比堆任何模型都更能让答辩现场的老师信服。希望帮到你。
本文还有配套的精品资源,点击获取