ARTICLE DETAIL

资讯详情

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

Python股市情绪分析实战:从股评文本到情绪指数与上证指数对比

Python股市情绪分析实战:从股评文本到情绪指数与上证指数对比 简介这是一份基于Python编程语言的股市市场情绪分析完整源代码与数据包面向量化研究者、金融数据分析师以及Python自然语言处理技术学习者。项目完整演示如何从互联网股评文本中提取投资者情绪并进一步构建情绪指标、研究情绪与股市走势之间的关联尤其适合关注A股非有效市场特征的读者。压缩包共15个文件涵盖4个Python脚本情感模型训练、情绪指标计算与可视化、4个CSV数据文件大盘行情、模型评分等、2个TXT标注语料积极/消极股评、2个PNG结果图表、1个Markdown说明文档及LICENSE许可文件整体约49.31MB目录结构清晰。目前已有2002人学习下载。资源内置传统机器学习与深度学习两套情感分析模型并配套股评原始数据、正负向标注语料、情绪指数生成以及与上证指数sh000001对比绘图脚本可直接运行复现“语料标注—情感分析—情绪指标—股市关联”的完整流程适合用于课程设计、毕业设计或量化策略研究的参考基线。1. 基于 Python 的股市市场情绪分析拆开 ZIP 后先看数据再谈模型如果你拿到的是一个叫stocksentimentanalysis的目录里面放着model_ml.py、model_dl.py、compute_sent_idx.py以及data下的stock_comments.zip、positive.txt、negative.txt和sh000001.csv那这套东西大概率不是“打开就出金叉”的玩具代码而是一条研究型管线先把互联网股评文本转成情感标签再聚合成情绪指标最后去对比上证指数走势。基于 Python 的股市市场情绪分析听起来很玄核心动作其实只有三个读文本、算情绪、对齐行情。这份资源适合课程设计、量化入门实验、金融文本分析练习不太适合直接拿去实盘。原因很简单情绪指标的样本外稳定性需要你自己验证源码给的是骨架不是圣杯。我会按文件布局拆开讲再给一条可复现的跑通路径每一步都落在具体函数和参数上。2. 情绪语料到监督数据先搞清楚 positive.txt 和股评怎么配对很多人一上来就打开深度学习模型文件结果被一堆 shape 和 embedding 绕晕。我拿到这类项目的第一反应是先翻data目录因为模型只是上层建筑语料和标签才是地基。这套资源的原始设计意图很明确用已经标注好的正负情感语料去分析股评文本再把分析结果变成指标。所以你得先回答一个问题positive.txt、negative.txt和stock_comments究竟是怎么拼成训练集的2.1 项目文件布局里谁是“食物链”底层假设你解压stock_comments.zip后得到的是按日期或板块组织的评论文件它们与positive.txt、negative.txt构成文本侧输入sh000001.csv是行情侧输入model_ml.py和model_dl.py负责训练分类器compute_sent_idx.py负责把预测结果转成时间序列plot_sent_idx.py则把情绪指数与上证指数画在一起。下面这个清单我建议先贴在笔记里文件路径在管线里的角色常见格式data/stock_comments.zip原始股评语料待分析对象ZIP 内为 TXT 或 CSVdata/positive.txt正面情感词或正面样本每行一条短语/句子data/negative.txt负面情感词或负面样本每行一条短语/句子data/sh000001.csv上证指数历史行情CSV至少含日期与收盘价model_ml.py传统机器学习情感分类Python 脚本model_dl.py深度学习情感分类Python 脚本compute_sent_idx.py聚合出情绪指数Python 脚本plot_sent_idx.py可视化对比Python 脚本这里有个常见误区positive.txt和negative.txt不一定是标准词典也可能是一行一个样本的“种子语料”。无论哪种你要做的第一步都是先把它们读成带标签的结构化数据。如果直接拿去当分词词典遇到“不涨反跌”“利好出尽”这种句子词典法会死得很难看。2.2 文本读取不能默认 UTF-8先做编码回退我从网上下载过的中文语料包里十个里有六个是 GBK 或 GB18030 编码。positive.txt和negative.txt的作者当年可能用 Windows 记事本保存直接with open(path, encodingutf-8)大概率在第三行就抛UnicodeDecodeError。我的习惯是写一个带编码回退的读取函数def read_text_lines(path: str) - list[str]: 读取中文文本文件优先 UTF-8失败后回退到 GB18030。 for enc in (utf-8, gb18030): try: with open(path, r, encodingenc) as f: return [line.strip() for line in f if line.strip()] except UnicodeDecodeError: continue # 最后一招用 ignore 兜底但只保留长度大于 1 的行 with open(path, r, encodingutf-8, errorsignore) as f: return [line.strip() for line in f if line.strip()] pos_lines read_text_lines(data/positive.txt) neg_lines read_text_lines(data/negative.txt)逻辑很简单先按严格模式尝试utf-8失败再试gb18030。gb18030兼容 GB2312 和 GBK中文互联网老旧文本基本都能覆盖。最后的errorsignore是保底方案但我不建议一上来就用因为它会静默吞掉无法解码的字节而中文里吞掉一个“不”字整条评论的情感极性可能直接反转。errorsreplace会换成替代字符至少你能在结果里发现异常行。2.3 把原始评论拼接成监督样本日期、文本、标签三字段读完词典或种子语料之后需要把stock_comments里的评论变成可以训练的数据。现实中的评论文件结构有两种一是每个日期一个文件夹里面若干 TXT 行二是一个大的 CSV每行带时间戳和正文。我习惯写一个兼容函数from pathlib import Path import pandas as pd def load_comments_to_frame(base_dir: str) - pd.DataFrame: 递归加载评论目录兼容 CSV 与 TXT并尽量保留日期线索。 frames [] for path in Path(base_dir).rglob(*.csv): # on_bad_lines 在 pandas 2.x 中替代了 error_bad_lines tmp pd.read_csv(path, encodingutf-8, on_bad_linesskip) frames.append(tmp) for path in Path(base_dir).rglob(*.txt): lines read_text_lines(str(path)) if not lines: continue # 常见结构文件名为日期例如 20240115.txt date_part path.stem tmp pd.DataFrame({content: lines, date: date_part}) frames.append(tmp) if not frames: raise ValueError(f{base_dir} 下没有找到 CSV 或 TXT 评论文件) df pd.concat(frames, ignore_indexTrue) # 这里只做最小清洗去掉空串和纯符号 df df[df[content].str.len() 1] return df comments load_comments_to_frame(data/stock_comments)这段代码的关键在on_bad_linesskip和文件名字段回填。前者是应对 CSV 里某行多了逗号导致列数错乱后者解决“没有时间字段的 TXT 怎么对齐行情”的问题。如果你的评论文件是单一大 CSV字段名大概率是date和content如果是按日期命名文件就把文件名当作日期。这两种情况我都遇到过写成一个通用入口能省很多事。有了comments之后再和positive.txt、negative.txt配对。注意如果正负文件是“词”而非“完整句子”那就不能直接 merge而要把它们当特征词典用。我会用df[content].str.contains(|.join(pos_words))生成一个弱标签但这种弱标签只能当 baseline别指望它有太高准确率。真正的训练标签最好来自原始评论里已经人工标注过的部分或者你自己抽 500 条做标注再用半监督方式扩标签。源码里如果没有现成训练集这条路径是最稳的。3. 从模型到情绪指数model_dl.py 与 compute_sent_idx.py 的分工文本数据准备好后管线进入第二个阶段训练分类器然后把分类器输出的逐条预测分数聚合成为日频情绪指标。这个阶段最容易让人误判模型越复杂不代表指标越有用。我要先把传统机器学习和深度模型在“这个项目里到底各自干什么”讲清楚再看情绪指数怎么与行情挂钩。3.1 为什么源码里通常同时存在两个模型文件大多数课程设计性质的项目不会只放一个模型因为作者想展示两条路线。model_ml.py走的是“词表 分类器”路线对评论分词、去掉停用词、用 TF-IDF 或词袋向量化再丢给逻辑回归、朴素贝叶斯或 SVM。model_dl.py走的是嵌入式表示路线把文本转成词向量序列经过循环网络或卷积网络提取特征最后输出一个 0 到 1 之间的情感分。我拆过的类似项目里model_ml.py不一定比model_dl.py差。原因在于股票评论的文本长度通常很短很多句子就是“今天涨了”“垃圾股”“看好后市”TF-IDF 能直接抓到这些强信号词。而深度模型在小样本上容易过拟合尤其是训练语料只有几千条时。所以我的建议是先跑model_ml.py拿到一个可解释的准确率基线再让model_dl.py去挑战这个基线。如果深度模型比传统模型高不到 2 个点你就该怀疑是数据量不足而不是模型不够强。model_dl.py内部常见的结构是这样Embedding 层把词索引映射成稠密向量再接双向 LSTM 或 GRU最后接一个 sigmoid 输出。用 Keras 风格写出来大概是from tensorflow.keras.models import Sequential from tensorflow.keras.layers import Embedding, Bidirectional, LSTM, Dense, Dropout from tensorflow.keras.optimizers import Adam model Sequential() model.add(Embedding(vocab_size, 128, input_lengthmax_len)) model.add(Bidirectional(LSTM(64, dropout0.3, return_sequencesFalse))) model.add(Dense(64, activationrelu)) model.add(Dropout(0.2)) model.add(Dense(1, activationsigmoid)) model.compile(optimizerAdam(learning_rate1e-3), lossbinary_crossentropy, metrics[accuracy])参数是我常用的起始值embedding_dim128对几万词规模的词表足够LSTM(64)是单元数太小欠拟合太大在小样本上容易记住噪声dropout0.3用来压过拟合。vocab_size和max_len需要从数据里统计出来前者是去重后的词数量后者是截断长度。如果评论平均 20 个字max_len设 50 就够设 300 只会让模型花大量参数去学填充位置。3.2 compute_sent_idx.py 的核心把离散预测变成连续序列模型训好之后compute_sent_idx.py要做的事不是画图而是“聚合”。单条评论预测出 0.82 或者 0.35 没有直接意义真正有意义的是某一天所有股评的平均情感分数。我一般会按日期分组然后做三件事取均值、做滚动平滑、差分或标准化。def compute_sentiment_index(score_df, date_coldate, score_colscore, window5) - pd.Series: 把逐条情感分数聚合为日度情绪指数。 s score_df.groupby(date_col)[score_col].mean().sort_index() # 滚动均值降低单日噪声 smoothed s.rolling(window, min_periods1).mean() # 变化率类似动量比原始均值更常与行情同步 sent_idx smoothed.pct_change().fillna(0) # 也可以换成多空比上涨/下跌评论数量之比 # bull score_df[score_df[score_col] 0.5].groupby(date_col).size() # bear score_df[score_df[score_col] 0.5].groupby(date_col).size() # sent_idx (bull / (bull bear)).reindex(s.index).fillna(0.5) return sent_idx sent compute_sentiment_index(score_df, window5)参数window5是权衡项取小了情绪序列毛刺很多取大了信号过于平滑滞后明显。我习惯先试 3、5、10 三档再用 IC 或相关系数选。pct_change()会把均值序列转成一阶变化率这比直接使用均值更能反映“情绪边际变化”因为股市反应的是边际增量而不是绝对水平。如果你看BI.png那个所谓的 BI 大概率就是类似思路做出来的指数。3.3 情绪与行情关联领先还是同步要用错位相关去看拿到情绪指数后plot_sent_idx.py会把情绪序列和sh000001.csv的收盘价画在同一张图里。看图能看出趋势但判断不了领先关系。我建议额外算一个“错位相关”把情绪序列向前移若干天与上证指数收益率计算 Pearson 相关看哪个滞后阶数相关性最高。def lead_lag_corr(sent, ret, max_lag10): sent 领先 ret 时 lag 为正lag 为负表示情绪滞后行情。 result {} for lag in range(-max_lag, max_lag 1): if lag 0: result[lag] sent.shift(-lag).corr(ret) else: result[lag] sent.corr(ret.shift(lag)) return result corrs lead_lag_corr(sent, sh000001[ret], max_lag10) best_lag max(corrs, keylambda x: abs(corrs[x])) print(fbest lag: {best_lag}, corr: {corrs[best_lag]:.3f})这里的ret必须用收益率而不是收盘价否则非平稳序列会产生大量虚假相关。lag0表示情绪指数和当日收益同步lag3表示今天的情绪指数与三天后的收益率相关性最高。如果best_lag稳定在正数且绝对值够大才算“情绪领先行情”。如果只有lag0显著那说明情绪只是行情的镜像没有预测价值。这一步虽然简单却往往是老师打分时最看重的地方因为它在论证“情绪能否指导投资决策”。4. 避坑手册编码、时间对齐和回测里最贵的三个“后悔药”所有数据类项目都有隐藏陷阱情绪分析尤其多因为文本处理和金融时间序列这两个领域各自有一堆历史包袱。我按自己的踩坑记录整理成“现象 → 原因 → 解决”三段式你可以直接对照排查。4.1 文本读不干净、极性判断反了的坑现象代码跑完positive.txt读出来全是乱码或者模型对“不看好”和“不好看”全都预测成负面。更诡异的是有些句子包含“利好出尽”居然被判成正面。原因第一层是编码问题前面说过UTF-8 严格模式遇到 GBK 文件会直接中断。第二层是中文否定词和转折结构问题。“不看好”的否定对象是“看好”而“不好看”的否定对象是“好看”两者语义差异很大用简单词典匹配时“利好”这个词权重太高把“出尽”的负面含义盖掉了。解决读取阶段坚决用编码回退不要errorsignore一把梭。在特征阶段要把否定词和后接 2 到 3 个字符拼成一个复合特征比如“不_看好”“未_反弹”。对于“但”“然而”这类转折词先按标点或连词分句再对后半句单独计算极性。model_ml.py里如果做的是 TF-IDF 加逻辑回归最好把 ngram_range 从(1,1)改成(1,2)或(1,3)这样“不看好”才有机会作为一个整体特征出现。命令行里的ngram_range参数看起来不起眼但对中文短文本的情感分类影响极大。4.2 评论时间与行情时间的对齐错位现象把情绪序列和上证指数画到一张图里发现情绪高点和指数高点完美重叠回测收益高得吓人但模拟盘跑起来却稳定亏钱。翻开代码发现情绪数据用的就是当天日期而当天评论里有大量下午三点以后发布的内容。原因这是典型的 look-ahead bias也叫未来函数。散户早上十点写“我看好”你把它算进当天情绪但如果你的模型在当天收盘前就要生成信号那些下午三点以后甚至晚上的评论根本来不及使用。即使只用当日收益率也会把“当天收盘后情绪”和“当天盘中走势”混在一起相关性和可交易性完全两回事。解决把评论时间戳做一次“交易日切分”。我用的规则是当天 15:15 之前发布的评论归属当天15:15 之后归属下一个交易日。import pandas as pd def align_comment_to_trade_date(ts: pd.Timestamp) - pd.Timestamp: 把评论时间对齐到交易日15:15 前算当天之后算次日。 day ts.normalize() if ts.time() pd.to_datetime(15:15).time(): day day pd.Timedelta(days1) return day df[trade_date] df[publish_time].apply(align_comment_to_trade_date)为什么是 15:15 而不是 15:00因为交易所收盘后还有集合竞价结果和少量公告给 15 分钟缓冲可以覆盖绝大多数盘后延迟。这个阈值你可以自己调整但原则不能变信号生成时点之前的信息才允许进模型。如果评论文件里只有日期没有时间那就保守一点全部因子向后挪一天再用。宁可损失相关性也不能引入未来数据。还有一个对齐坑sh000001.csv里的交易日是周一到周五遇到节假日会断档。评论数据如果不剔除周末情绪序列就会出现“周六空值”或“周日值为零”的现象。合并之前先ret_index ret_index[ret_index.index.isin(comment_dates)]把两端索引取交集避免NaN污染相关计算。4.3 小样本过拟合与标签分布漂移现象模型在训练集上准确率 0.96验证集准确率 0.93但拿到真实股评上预测一半以上都落在 0.5 附近完全分不出正负。原因标注语料和真实语料分布不一致。positive.txt里的句子往往很干净比如“后市看好”“业绩增长”但真实评论是“这票套了我一年”“主力洗盘真狠但我不想割”里面有情绪词和反讽、省略、错别字。用干净语料训练出的分类器在嘈杂文本上会失效。另一个原因是没有按时间划分训练集测试集而是随机打乱导致同一时期的评论同时出现在训练和测试里测试成绩虚高。解决先做时间序列切分早期 80% 当训练最近 20% 当验证。这个工程比换模型更有效果。其次从真实评论里随机抽 200 条自己快速标注一遍并入训练集微调这能把分布漂移拉回一大截。最后评估指标用roc_auc或宏平均 F1不用准确率因为“看涨”和“看跌”在情绪文本里样本比例经常失衡。看到model_ml_scores.csv时别只看均值打印出预测分位数分布。如果 90% 的预测值都在 0.4 到 0.6 之间说明模型输出高度不确定这时候强行把 0.5 当阈值就会产生大量随机分类。4.4 情绪指数的极值处理缺失现象某一天只有两条评论其中一条被预测为 0.99另一条为 0.01情绪变化率 pct_change 突然飙升到 500%画出来是一根尖刺还跟行情波动强相关你以为找到了信号实际上只是样本量噪声。原因groupby(date).mean()在样本量极小的时候均值极不稳定。单日 2 条评论和单日 500 条评论在统计意义上完全不能比但代码不会自动区分。解决在聚合函数里加一个min_count参数当日评论条数低于阈值就直接置空或者用全样本均值补缺。我一般取全样本日评论量的中位数低于中位数 20% 的日子视为无效样本。补一句要提醒的话rolling(window, min_periods1)里的min_periods1会让前期序列看起来特别平滑因为窗口没填满时用部分数据算均值。正式分析中我习惯把min_periods设为window宁可前面多几个空值也不让早期数据污染趋势判断。5. 验证与进阶用法跑通最小闭环先画图再谈结论任何情绪分析项目最终都逃不过两个问题这个情绪指标到底预测了什么换一段行情还成立吗源码里的BI_and_Market.png只是一个静态结果你自己能复现出那张图才算真正掌握了这套工具。我给自己的工作流定了一个最小闭环加载模型 →预测一撮真实评论 →聚合情绪指数 →画对照图 →看错位相关。五步跑完才允许下结论。第一步不用等完整数据手动写十条测试评论比如“今天放量突破后市看涨”“连续阴跌不敢加仓”“利好出尽就是利空”用model_dl.py里封装好的predict_text()打一遍分数。这一步验证模型有没有“活着”。如果十条分数全部是 0.5 附近回去检查 4.1 的预处理逻辑通常不是模型坏了是输入文本没进入预期格式。第二步才是全量评论跑批预测保存在score_df里。第三步调用compute_sentiment_index()得到情绪序列。第四步把情绪指数与上证指数收益率画在同一坐标系。这步很关键如果两张图一个走尖峰、一个走平线说明情绪与行情几乎无关你的研究结论就应该是“情绪无预测力”这也是有价值的结论。画图时我会把情绪序列做归一化除以滚动标准差这样尖峰不会撑爆 Y 轴。第五步计算错位相关只看绝对值大于 0.1 且滞后阶数稳定的结果。05 以下的相关系数默认视为噪声。进阶用法是把这个管线的输入换成你自己的数据源。最常见的改造是把stock_comments换成雪球或东财的评论导出文件把sh000001.csv换成任意指数或个股日线。只需要保证评论有日期、行情有日期、日期对齐规则统一其他代码都不用动。如果你想做得更深可以把“单日平均情绪”升级成“情绪分歧度”计算每日预测分的标准差标准差高说明市场分歧大这本身也是一个有意思的研究维度。最后说一个我自己的习惯供你参考以前我跑完模型只看回测收益后来被实盘打脸后才明白情绪分析最怕的不是模型精度不够而是“看起来相关、实际上不可交易”。从那以后我每一次拿到新数据都会强制走一遍最小闭环先把情绪指数和行情图原样复现出来再谈下一步优化。图和数字不会骗人但如果只看数字很容易被自己骗。这套源码的价值就在于此它给了你一个标准流程而不是一个标准答案。希望帮到你。本文还有配套的精品资源点击获取
返回列表