简介:面向金融科技与机器学习学习者,这份资源围绕股评文字与上证指数历史数据,完整演示了从互联网提取投资者情绪、经情感分析和指标构建,最终量化看涨情绪与股市走势关系的技术路径。资源包共16个文件,以csv数据文件、py模型脚本、txt情感词典、bigram-char预训练词向量文件以及docx说明文档为主,压缩包整体约654.97MB;数据与脚本配套齐全,下载后即可复现从原始股评到情绪指数合并、模型训练与对比的全流程。目前已有807人浏览学习。读者可获得已分词的股评语料、上证指数行情数据、正负情感词典、预训练词向量、情绪指数计算脚本、深度学习与机器学习模型实现,以及对应的说明文档,既适用于课程设计、论文实验,也可作为金融NLP入门项目的完整参照模板。
1. 从一条股评到一个涨跌信号:股市情感分析到底在做什么
假如你在理财社区看到“放量突破,看多”和“高位跳水,别追”两条股评,能不能用 Python 把它们变成次日涨跌的预测信号?这个项目给出的答案是:先做情感分析,再借助词向量构造看涨情绪指标,最后交给机器学习模型去验证情绪与上证指数走势之间的关系。它自带分词语料、情感词典、预训练词向量和完整脚本,不用再四处找数据,适合想做金融 NLP、量化入门或者课程设计选情感分析方向的人。整个流程不复杂,但每一步都有容易踩的坑,下面按我拆这个项目时的顺序来讲。
2. 数据准备与词向量加载:从原始评论到分词向量
要从文本里提取情绪,首先得把原始评论变成计算机能处理的形态。项目里data目录同时保留了原文和分词结果,这个设计很实用:原文用来核对情感词,分词结果可以直接送给后续模型,省去重复分词的时间。
2.1 先看清数据里的四个文件
我拿到数据集后习惯先打开文件看前几行,而不是急着跑脚本。stock_comments_raw.csv是财经社区的股评原文,核心字段是日期和评论正文;stock_comments_seg.csv是同一批评论经过 jieba 分词后的结果,词语之间用空格分隔;sh000001.csv是上证指数日线行情,至少包含日期、收盘价和成交量;positive.txt和negative.txt是情感种子词典,每行一个词。这几个文件的关系是:前两个提供情绪,第三个提供市场结果,最后两个提供情感分析的基础。
| 文件 | 类型 | 关键字段 | 用途 |
|---|---|---|---|
| stock_comments_raw.csv | 文本 | date, content | 原始评论,便于人工检查 |
| stock_comments_seg.csv | 文本 | date, seg_content | 分词后语料,直接进入情感得分 |
| sh000001.csv | 行情 | date, close, volume | 计算收益与对齐样本 |
| positive.txt / negative.txt | 词汇表 | 每行一个词 | 情感词典种子词 |
检查完字段后再谈处理。常见的坑是评论里混有换行、空评论、重复转发,这些都会在分句和统计时变成噪音。我一般会在预处理阶段去掉 URL 和@用户,只保留中文、数字和常用标点,并过滤长度小于两个字的评论。分词结果文件可以跳过这一步,但从原始评论复现流程时别漏掉。
2.2 用 jieba 分词并保护金融术语
stock_comments_seg.csv已经分词,但若要重新复现,还是得从 raw 再切一遍。原因很简单:金融领域的专有名词和口语化表达,通用分词词典常常切错。比如“看多”如果被 jieba 切成“看/多”,两个单字都无法命中情感词典,信号就丢了。项目中可以用jieba.load_userdict加入自定义词表,把“看多”“北向资金”“创业板指”这类词固定下来。
# tokenize.py import jieba import pandas as pd jieba.load_userdict("userdict.txt") # 每行一个自定义词 df = pd.read_csv("data/stock_comments_raw.csv") def cut_text(text: str) -> str: return " ".join(w for w in jieba.lcut(text) if w.strip()) df["seg_content"] = df["content"].apply(cut_text) df[["date", "seg_content"]].to_csv("data/stock_comments_seg.csv", index=False)这段代码用pandas读入原始评论,jieba.lcut返回分词列表,再用空格拼接成文本。userdict.txt可以维护财经领域词,例如“放量突破”“补涨”“砸盘”。这么做的原因是 jieba 默认词典来自通用语料,对“突破”“板块”这类词虽然能切出来,但口语化词经常被切碎,保护词条后情感得分会更稳定。
2.3 读取 sgns.baidubaike.bigram-char 词向量
项目自带的词向量文件data/sgns.baidubaike.bigram-char是百度百科语料上训练好的预训练向量,格式与 word2vec 文本格式一致,常见维度是 300。加载后每个词都有稠密向量,之后用词向量扩充情感词典全靠它。读取时用 gensim 最省事,但要注意文件较大,内存占用需提前规划。
# load_w2v.py from gensim.models import KeyedVectors w2v_path = "data/sgns.baidubaike.bigram-char" w2v = KeyedVectors.load_word2vec_format( w2v_path, binary=False, unicode_errors="ignore" ) print("词表大小:", len(w2v.key_to_index)) print("维度:", w2v.vector_size)binary=False表示明文文本格式,unicode_errors="ignore"可以跳过非 utf-8 字符。如果运行机器内存有限,几十万词的向量文件容易吃满十几个 G 内存,可以改成limit=200000只取前 20 万常见词,但会牺牲生僻金融词。另一个办法是先用 gensim 加载,再只保存训练语料里出现过的词,既省内存又不影响后续相似度计算。语料和词向量就绪之后,下一步进入真正的情绪计算。
3. 情感打分与词典扩展:用词向量把“看多/看空”算出来
这里的核心不是直接上大模型,而是“情感词典 + 词向量余弦相似度”的组合。股市评论普遍短、口语化、时间跨度长,人工标注成本高,只靠positive.txt和negative.txt里几百个词,根本覆盖不住“冲高回落”“尾盘拉抬”“妖股”这些表达。用词向量扩展情感词表,是成本和效果之间最稳的平衡点。
3.1 为什么通用词典在股票语料上会失灵
通用情感词典会把“涨”“暴跌”“风险”分别归入积极和消极,但股票评论里“风险”不一定是负面,“放量”经常被当作利好。同一句话里还可能出现“不是看多,是不敢做空”这种转折。所以项目采用的方法是:用种子词典确定情感极性方向,再用词向量计算语料里每个词与种子词的相似度,自动扩出一个更贴合股票语境的词表。
3.2 用余弦相似度扩展情感词表
具体操作是,对语料里出现过的每一个词,计算它与正向种子词集合的平均相似度、与负向种子词集合的平均相似度,两个相似度的差值决定这个词的极性。比如“拉升”和“上涨”在向量空间中距离很近,于是会被赋予正极性;“杀跌”会被归入负极性。这个步骤在compute_sent_idx.py里相当于一个预处理函数,先跑一次生成新词典,再打分。
# expand_lexicon.py import numpy as np from gensim.models import KeyedVectors def load_lexicon(path): words = set() with open(path, encoding="utf-8") as f: for line in f: words.add(line.strip()) return words def expand_sentiment_lexicon(corpus_terms, pos_words, neg_words, w2v, threshold=0.4): new_pos, new_neg = set(pos_words), set(neg_words) for term in corpus_terms: if term not in w2v or term in new_pos or term in new_neg: continue pos_sim = np.mean([w2v.similarity(term, s) for s in pos_words if s in w2v]) neg_sim = np.mean([w2v.similarity(term, s) for s in neg_words if s in w2v]) if pos_sim - neg_sim > threshold: new_pos.add(term) elif neg_sim - pos_sim > threshold: new_neg.add(term) return new_pos, new_neg代码的关键是w2v.similarity会先做归一化再算余弦距离,所以结果天然在[-1, 1]区间。threshold控制扩词的激进度,值越小纳入的词越多,但误判也越多。我一般先用 0.4 跑一轮,再人工抽检扩充结果。注意pos_words里如果混入不在词向量中的词,要用if s in w2v过滤掉,否则np.mean会报错。这一步产出的新正负词典,是后续打分的基础。
3.3 逐条评论计算情感得分
有了词典,打分不能只数关键词次数,还要处理否定词和程度副词。项目里的compute_sent_idx.py我看到的做法是:先判断窗口内是否有“不”“没”“别”这类否定词,如果有,情感极性反转;再给“强烈”“继续”“小幅”这类程度词赋权重。最后用句子中正负极性的加权和除以命中词数,得到一条评论在[-1, 1]之间的情感分。
# compute_sent_idx.py 核心逻辑 def sentence_sentiment(seg_tokens, pos_dict, neg_dict, deny_words): score = 0.0 hit = 0 deny = False for tok in seg_tokens: if tok in deny_words: deny = True polarity = 0 if tok in pos_dict: polarity = 1 elif tok in neg_dict: polarity = -1 if polarity != 0: if deny: polarity = -polarity deny = False score += polarity hit += 1 return score / max(hit, 1)对每条分词后的评论,遍历词项并累加极性,deny只反转最近一个情感词的极性,而不是反转整句话。这个细节很关键,因为“不看好”和“不好看”在股市语境里意思完全不同。随后把 date、seg_content、score 一起写入stock_comments_analyzed.csv。输出片段如下。
| date | seg_content | sent_score | 主要情绪词 |
|---|---|---|---|
| 2020-07-03 | 放量 突破 看多 | 0.62 | 看多,突破 |
| 2020-07-03 | 高位 跳水 别追 | -0.48 | 跳水,追 |
人工抽查这张表是验证情感分析是否可靠的第一步。如果某一天 score 全部为 0,说明词典没覆盖到当天热门词,需要回到 3.2 重新扩词;如果全是极端值,多半是threshold太低,把语气词也当成了情感词。这里还要注意标签泄漏:情感打分只能用当天评论和词典,不能掺入未来数据,否则后面训练的模型准确率再高也是假的。
4. 指标构建与机器学习建模:随机森林与深度模型对比
有了逐条评论的情感分,还不能直接塞进模型。一天里的评论有多有空,时间维度上还有波动,需要把这些信息压缩成少数几个日度指标,再对齐上证指数。merged_sentiment_idx.csv就是干这个用的。
4.1 日度看涨情绪指标:不要只取均值
最常见的错误是每天取一个情感均值就当作特征。均值会掩盖信息:某天 10 条评论全是 0.6,另一天 1000 条评论一半 0.6 一半 -0.6,两者均值接近,但市场含义完全不同。因此要同时构造三个指标:看涨比例、强度、变化率。
| 特征名 | 定义 | 为什么有效 |
|---|---|---|
| bull_ratio | 正向情感评论数 / 当日总评论数 | 描述市场分歧度 |
| sent_strength | 当日情感得分均值,剔除0分评论 | 情绪强弱 |
| sent_change | 当日强度 - 前5日均值 | 捕捉情绪拐点 |
计算时我直接对stock_comments_analyzed.csv按日期分组聚合,再合并到sh000001.csv上。注意评论在周末没有记录,股市周末也不开市,所以要用外连接把情绪日期和交易日对齐,缺失指标向前填充。
# build_features.py import pandas as pd comments = pd.read_csv("stock_comments_analyzed.csv", parse_dates=["date"]) daily = comments.groupby("date")["sent_score"].agg( bull_ratio=lambda x: (x > 0).mean(), sent_strength=lambda x: x[x != 0].mean(), comment_cnt="count", ).reset_index() idx = pd.read_csv("sh000001.csv", parse_dates=["date"]) df = pd.merge(idx, daily, on="date", how="left") df["sent_change"] = df["sent_strength"] - df["sent_strength"].rolling(5).mean()这段代码用groupby聚合出每日指标。rolling(5).mean()是过去 5 个交易日的平均情感强度,用它做差分相当于一阶动量。这里尤其要注意:不要用全样本均值做标准化,否则会把未来信息带进当前值,测试集就失去意义。全样本标准化相当于训练时已经“看见”了未来的统计量,上线后效果会明显缩水。
4.2 构造标签和对齐时间窗
情感指标应该领先于价格,标签用未来收益而不是当日收益。model_ml_scores.csv的生成逻辑应该是:用 T 日的情绪指标预测 T+1 日上证指数是否收涨。T 日收盘后评论集中出现,T+1 日开盘前我们手里有全部情绪数据,所以这个设计没有未来函数,属于时间错位设计。
df["ret_next"] = (df["close"].shift(-1) > df["close"]).astype(int) df = df.dropna(subset=["ret_next"])close.shift(-1)把明日收盘价移到今日,而不是用今日收盘价计算当日收益。时间序列回测里最容易错的就是这里:当日收益在当天收盘前是不知道的,用当日收益做标签等于作弊。特征和标签构造完成后,只保留有交易的日子。
4.3 用随机森林训练一个可解释的分类器
model_ml.py用的是scikit-learn里的分类器。情感分析衍生的特征通常很少,树模型比线性模型更容易捕捉特征之间的交互。随机森林的优势在于可以输出特征重要性,方便看出“情绪拐点”是否比“情绪绝对水平”更有用。训练时用TimeSeriesSplit,不要随机打乱样本。
# model_ml.py 简化 from sklearn.ensemble import RandomForestClassifier from sklearn.model_selection import TimeSeriesSplit from sklearn.metrics import roc_auc_score feature_cols = ["bull_ratio", "sent_strength", "sent_change", "comment_cnt"] X = df[feature_cols].values y = df["ret_next"].values tscv = TimeSeriesSplit(n_splits=5) for i, (tr_idx, te_idx) in enumerate(tscv.split(X)): clf = RandomForestClassifier(n_estimators=200, max_depth=4, random_state=42) clf.fit(X[tr_idx], y[tr_idx]) proba = clf.predict_proba(X[te_idx])[:, 1] print("fold", i, "AUC", roc_auc_score(y[te_idx], proba))TimeSeriesSplit按时间顺序切分训练集和验证集,避免随机采样造成未来信息泄漏。max_depth=4限制树深度,防止特征很少的情况下树模型仍然学到局部噪音。AUC 如果稳定超过 0.52,说明情绪指标对次日涨跌有微弱但真实的影响。要提醒的是,股票预测的 AUC 通常不会很高,0.55 已经是很有信息量的结果,不要用准确率高低来判断项目好坏。
4.4 深度学习基线:用预训练词向量再做一次验证
model_dl.py是另一个视角:不手动构造特征,而是把分词后的评论文本用预训练词向量映射成序列,再用 LSTM 或 TextCNN 预测次日涨跌。这样做的价值是验证情感词典漏掉的语义是否可能被神经网络捕获。小数据集上模型要尽量轻,加载词向量后最好冻结 Embedding 层。
# model_dl.py 简化(Keras样例) from tensorflow.keras.layers import Embedding, LSTM, Dense, Input from tensorflow.keras.models import Model max_len = 50 inputs = Input(shape=(max_len,)) x = Embedding(vocab_size, 300, weights=[embedding_matrix], trainable=False)(inputs) x = LSTM(64, dropout=0.3)(x) outputs = Dense(1, activation="sigmoid")(x) model = Model(inputs, outputs) model.compile(optimizer="adam", loss="binary_crossentropy")weights用预训练矩阵初始化 Embedding,行号对应词表索引。trainable=False防止小数据把预训练向量改坏。LSTM 输出 64 维隐状态后接 sigmoid,直接输出次日上涨概率。深度模型在小语料上一般不会超过随机森林太多,但它的意义是用无监督预训练词向量完成了对情感词典的补充验证。如果两个模型的预测曲线出现重大分歧,优先检查日度聚合口径,而不是调模型结构。
5. 验证情绪指标:IC、分组收益与双轴可视化
机器学习模型可以输出 AUC,但要研究情绪与股市的关系,还得回答“情绪指标本身”有没有预测力。我的习惯是先算信息系数,再画分组收益图。
5.1 用 Spearman 相关检查单调关系
情感指数的取值是不是越大越好,用 Pearson 相关容易被极端值影响,换成 Spearman 更稳健。把sent_strength和次日收益放在同一张表里,计算秩相关。
from scipy.stats import spearmanr rho, p = spearmanr(df["sent_strength"], df["ret_next"]) print("IC =", rho, "p =", p)rho的正负告诉你看涨情绪是领先还是滞后,p值在样本量不大时有参考意义。如果 p 值很高,说明该周期内指标与收益没有明显关系,需要重新审视聚合窗口,例如把“一日”改成“三日”再试一次。
5.2 分组收益检验
信息系数只说明单期相关性,实际操作中更关心情绪最高的一组次日收益是否显著高于情绪最低的一组。把样本按sent_change分成 5 组,计算每组下一日平均收益。
df["sent_q"] = pd.qcut(df["sent_change"], 5, labels=False) group_ret = df.groupby("sent_q")["ret_next"].mean() print(group_ret)如果从低到高呈现单调递增,说明情绪拐点指标有真实区分度;如果出现倒 U 形,说明极端情绪时市场反而容易反转。这个结果比 AUC 更容易解释给非技术同学。
5.3 用双轴图直接看情绪与走势
最后用项目里plot_sent_idx.py的思路画一张双轴图,左轴是上证指数收盘价,右轴是情绪强度。
import matplotlib.pyplot as plt ax = df.plot(x="date", y="close", color="black", linewidth=1) ax2 = df.plot(x="date", y="sent_strength", color="red", secondary_y=True, ax=ax) plt.show()secondary_y=True让情绪强度使用右侧坐标轴,否则情绪强度在 0 附近波动,会把指数曲线压成一条直线。画完之后重点看情绪顶部和指数顶部的先后顺序:如果情绪反复领先指数一两天,后面建模时尝试把shift(-1)的标签改成shift(-2),往往能小幅抬升 IC。把这两条曲线叠在一起看,比看任何准确率指标都直观。
本文还有配套的精品资源,点击获取