本章会用到的数据
公开下载 :上市公司审计意见 。本章使用披露日期、审计机构和意见类型等结构化字段。
这些数据能做什么 :练习中文分词、词袋模型和 TF-IDF 分类。
分析时注意 :本章分析的是结构化字段,不等同于对完整年报正文做情感分析。
判断模型 :用较早年份训练、下一年验证,最后在未参与选择的年份检查一次。
【课堂核心】3 学时学习安排
动机与先修
20
BOW、TF-IDF 检查与反馈
45
相似度边界与检查
40
独立练习
55
反馈与小结
20
【可选拓展】 :除学习安排已列出的阿里巴巴短引文与正式引用外,其他正文片段、情感词典、词嵌入和外部工具留作课后阅读,不属于本章学习要求。
先修入口检查:向量、折与标签
限时 6 分钟,独立提交三行:
向量、L2 范数与余弦相似度的含义。
为何词表、IDF 与缩放必须在每个训练折拟合。
文本 info_date、决策日与下一财年标签的先后顺序。
提交前不看下一页。
先修反馈与即时订正
向量层 :向量是有序特征权重,L2 范数表示长度,余弦衡量方向。
验证层 :词表或 IDF 若读到验证、测试文本,就会产生信息泄漏。
时间层 :先取得决策日前文本,再在未来可得日生成标签。
即时订正 :概念漏项回看向量范数页;折内漏项回看第 6 章;时点倒置先重画时间线。
欢迎来到文本分析的世界
从财报到候选文本信号
财报语言可以被量化为可检验的文本特征,但是否具有样本外预测力必须在明确的信息时点、交易成本和最终测试期下验证。本章不再保留来源不明的“5% 超额收益”数字。
语言,不再仅仅是描述,它本身就是数据。
从文本到信号
一个文档图标通过量化分析转化为待检验候选文本特征;图形不代表已实现 alpha 或投资收益。
10-K
量化分析
文本信号
本章学习目标:核心问题
核心问题: 公司的年报、新闻稿和分析师报告中充满了重要的非结构化信息。我们如何系统性地、大规模地将这些文本转化为可用于金融建模的量化信号?
本章学习目标:能力养成
本章结束后,你将能够:
解释 BOW 与 TF-IDF 的精确计算口径。
构造 折内拟合的中文字符 n-gram 分类流程。
比较 扩展窗口模型与多数类基线,并解释混淆矩阵。
诊断 零向量、词序丢失与时间泄漏等结果不理想时如何解释。
说明 本章使用审计机构与意见类型构造的结构化字符串,未完成真实公告正文情感分析。
本章能力目标图示
五个图标,分别代表情感分析、文本预处理、相似度计算、词嵌入和编程实现。
😃/😠
1. 情感分析
🧼
2. 文本预处理
📏
3. 文本相似度
🧠
4. 词嵌入
👨💻
5. Python编程
金融文本无处不在,蕴含巨大价值
在金融市场,语言是传递信息的关键媒介。
公司年报/季报 (10-K/10-Q)
管理层对经营现状的总结和未来展望 (MD&A)。
新闻稿 (Press Releases)
关于并购、新产品发布、盈利预警等重大事件的即时信息。
分析师报告
专家对公司财务状况和未来前景的独立评估。
社交媒体 / 新闻
市场情绪、突发事件和公众舆论的实时反映。
核心挑战:从非结构化到结构化
‘Despite macroeconomic headwinds, our cloud division saw record growth, though litigation risks remain a concern…’
本章任务: 我们将学习如何搭建一座桥梁,将左边的非结构化文本,转化为右边的结构化特征向量(Feature Vectors)。
我们的路线图:从计数表示到待验证的上下文表示
我们将分三步,逐步深入地将文本转化为数字。
文本分析路线图
一个三步走的路线图,从词袋模型到文本相似度,再到词嵌入,展示了分析深度的递进。
1. 词袋模型
将文本视为词汇的集合
统计词频,简单直接
2. 文本相似度
将文本表示为向量
计算向量间的“距离”
3. 词嵌入
从上下文学习词义
学习上下文相关表示
第一部分:词袋模型 (Bag-of-Words)
词袋模型(简称 BOW)是一种透明、可检查的文本计数基线;它不是所有现代文本表示的必要前置。
核心思想: 忽略文本中的语法和词序,将其简单地视为一个装满词汇的“袋子”,然后统计袋子中每个词汇出现的次数。
词袋模型图解
一句话被分解成独立的词汇,然后落入一个象征性的袋子中,顺序和语法被忽略。
原始句子: The quick brown fox jumps.
The
quick
brown
fox
jumps
词袋
fox
The
jumps
quick
brown
这个简单模型为本章的 TF-IDF 与相似度练习提供透明计数基线;其他表示方法是否适用取决于语料、任务与样本外验证。
案例:阿里巴巴 2024 财年年报摘录
我们以来自总部位于杭州、在香港联交所上市的阿里巴巴集团 2024 财年年报句首节选为例,贯穿词袋与 TF-IDF 机制讲解。
“Our data centers employ leading technologies in distributed fault-tolerant architecture, advanced in-house power and cooling equipment, AI-driven intelligent monitoring and operational technology,”
BOW第一步:文本预处理 (Preprocessing)
原始文本是“脏”的,无法直接使用。我们需要一个“清洗”过程,也就是文本预处理。这个过程的目标是降噪 和标准化 。
文本预处理流程
一个展示文本预处理五个步骤的线性流程图:分词、转小写、词形还原、去停用词、特殊处理。
'The cat sat...'
原始文本
1. 分词
2. 标准化
(转小写等)
3. 词形还原
4. 去停用词
5. 特殊处理
['cat', 'sit']
干净词表
预处理1:分词 (Tokenization)
分词是将连续的文本字符串分解成一个个有意义的单元(tokens)的过程。
英文分词: 相对简单。主要依据空格和标点符号进行分割。
中文分词: 更加复杂。因为中文词与词之间没有天然的分隔符。需要依赖专门的算法和词库。
中英文分词对比
对比英文和中文分词的过程和难点,英文基于空格,中文需要算法识别词边界。
英文分词 (简单)
"Profit grew quickly."
Profit
grew
quickly
中文分词 (复杂)
"公司盈利增长"
✓ 正确切分
公司
盈利
增长
✗ 错误切分
公司
盈利增长
代码演示:使用jieba进行中文分词
jieba(结巴)是本章采用的 Python 中文分词实现之一;不同分词器须按语料、词典版本与下游任务比较。
展开示例代码
代码
# 导入 jieba,在当前文档中直接运行中文分词示例。
import jieba
# 为“代码演示:使用`jieba`进行中文分词”导入 `pandas` 并绑定 `pd`,用于整理当前任务的表格、字段与时间索引。
import pandas as pd
import numpy as np
# 待分词的中文句子
text_cn = '金融市场瞬息万变,我们需要有效的分析工具。'
# 使用jieba的lcut方法进行分词(返回一个列表)
tokens_cn = jieba.lcut(text_cn)
# 将结果用更清晰的方式展示
print (f'原始文本: { text_cn} ' )
# 打印分隔线,区分原文和分词后的逐词结果。
print ('---' * 15 )
# 标明随后输出的是分词 token,而非原始连续文本。
print ('分词结果 (Tokens):' )
# 使用Pandas DataFrame美化输出
# 每个 token 占一列值,便于检查中文分词边界。
df_tokens = pd.DataFrame(tokens_cn, columns= ['词汇 (Token)' ])
# 横向打印 token 表,保留课堂投影所需的紧凑布局。
print (df_tokens.T.to_string(header= False ))
原始文本: 金融市场瞬息万变,我们需要有效的分析工具。
---------------------------------------------
分词结果 (Tokens):
词汇 (Token) 金融市场 瞬息万变 , 我们 需要 有效 的 分析 工具 。
预处理2&3:标准化词形
同一词根的词汇,因为时态、单复数等原因,会表现为不同形式。 invest, investing, invested, investments
为了让模型知道它们都指向同一个核心概念“投资”,我们需要将它们标准化为基本形式。
两种主要方法:
词干提取 (Stemming): 按规则截断,速度快,但可能产生非词。
词形还原 (Lemmatization): 借助词典、形态与词性映射返回 lemma;质量取决于语言、词典覆盖、词性与领域词汇,不保证更准确。
方法一:词干提取 (Stemming)
词干提取 是一种比较粗糙的标准化方法,它通过移除单词的后缀(有时是前缀)来将其简化为词干(stem)。
特点: 速度快,计算开销小,不依赖词典。
缺点: 结果不一定是真实的单词。
例如:
investments → invest
making → make
operating → oper (注意:oper 不是一个有效的英文单词)
studies → studi (同样不是有效单词)
方法二:词形还原 (Lemmatization)
词形还原 利用词汇、形态与词性信息,将单词映射到词典基本形式(lemma)。在资源覆盖且词性正确时,结果通常更可读;未知词、金融术语和词性错误仍会导致失败。
特点: 输出取决于目标语言、词典覆盖和词性标注。
缺点: 资源依赖与计算开销更大,不能保证下游任务更准确。
例如:
investments → investment
making → make
operating → operate
studies → study
better → good (词干提取无法处理这种情况)
Stemming vs. Lemmatization: 速度与精度的权衡
词干提取与词形还原对比
一个对比图,展示了'studies'一词经过词干提取和词形还原后的不同结果和过程,突出了速度与精度的权衡。
词干提取 (Stemming)
'studies'
移除后缀 "ies"
'studi'
✓ 速度快 | ✗ 结果可能无意义
词形还原 (Lemmatization)
'studies'
查询词典,找到原型
'study'
依赖词典与词性 | 开销较大
选择边界: 不存在普适推荐。应在目标语言与下游任务上比较;本章中文财报任务优先检查 jieba 分词、领域词典或子词表示,英文 WordNet 示例不构成默认方案。
小练习:比较原词与词典基本形式
下面用一张小表观察词形归并结果。真实分析应根据所用词典和文本任务检查结果,不能把示例映射直接套到其他语料。
展开示例代码
代码
# 使用本章已经导入的 pandas,把例词与基本形式整理成可核对的表格。
import pandas as pd
# 这些结果用于说明“多个词形可归到同一基本形式”,不是通用词典。
words = ['investments' , 'making' , 'operating' , 'services' , 'better' ]
basic_forms = ['investment' , 'make' , 'operate' , 'service' , 'good' ]
# 使用DataFrame美化输出
df_lemma = pd.DataFrame({
# 保留原始单词列,作为词形转换前的检查参照。
'原始词汇 (Original)' : words,
# 并列保存基本形式,便于逐行比较。
'词典基本形式 (Lemma)' : basic_forms
# 结束两列对照表的字段定义。
})
# 以 Markdown 表格输出原词与原型的逐项对照。
print (df_lemma.to_markdown(index= False ))
| 原始词汇 (Original) | 词典基本形式 (Lemma) |
|:----------------------|:-----------------------|
| investments | investment |
| making | make |
| operating | operate |
| services | service |
| better | good |
预处理4:去除停用词 (Stop Words)
停用词 是指在文本中频繁出现,但通常不携带重要信息的词汇。
英文停用词: a, an, the, is, in, on…
中文停用词: 的, 是, 了, 在, 和, 也…
在进行词频统计前,我们通常会移除这些词,以减少噪音,突出那些真正有意义的关键词 。
去除停用词过程
一句话中的停用词'The', 'in', 'are'被高亮并移除,只留下关键词'assets', 'portfolio', 'growing'。
The
assets
in
the portfolio
are
growing
assets portfolio growing
Filter
预处理5:特殊处理否定词 (Negation)
否定词可能改变极性和作用范围;是否以及如何改变,必须按具体任务验证。
'The company reported growth.'(在给定标注任务中可能偏正向)
'The company reported no growth.'(否定增长命题;极性仍依赖语境与标注规则)
简单的停用词删除可能丢失 no、not 等否定信息;将 'not good' 合并为 'not_good' 只是一种候选编码,须与保留原词序等方案在同一任务上比较。
确定词元说明:清洗表与向量必须同源
Tokenizer :用 [A-Za-z]+(?:-[A-Za-z]+)* 取词,保留词内连字号;因此 AI-driven、fault-tolerant、in-house 各是一个 unigram。
大小写与停用词 :先转小写;本句仅删除确定集合 {our, in, and}。
词形映射 :centers→center、leading→lead、technologies→technology、distributed→distribute、cooling→cool、monitoring→monitor;其余保留。
代码
# 导入正则表达式工具,按确定的连字号规则从年报节选取词。
import re
# 导入词频计数器,使 TF 表与清洗序列共用同一输入。
from collections import Counter
# 确定印刷页 40 的句首节选,不将省略的后文纳入计数。
alibaba_report_excerpt = "Our data centers employ leading technologies in distributed fault-tolerant architecture, advanced in-house power and cooling equipment, AI-driven intelligent monitoring and operational technology,"
# 固定只删除本例明示的三个功能词,避免静默丢弃内容词。
alibaba_stopwords = {"our" , "in" , "and" }
# 固定六个教学词形映射,其余词元保持原样。
alibaba_lemma_map = {"centers" : "center" , "leading" : "lead" , "technologies" : "technology" , "distributed" : "distribute" , "cooling" : "cool" , "monitoring" : "monitor" }
# 按“字母串+可选词内连字号”分词并统一转为小写。
alibaba_raw_tokens = re.findall(r" [A-Za-z] + (?:- [A-Za-z] + )* " , alibaba_report_excerpt.lower())
# 先移除确定停用词,再对剩余 unigram 应用显式词形映射。
alibaba_clean_tokens = [alibaba_lemma_map.get(token, token) for token in alibaba_raw_tokens if token not in alibaba_stopwords]
# 从清洗序列直接生成词频,禁止另手工填写一套 TF 口径。
alibaba_term_frequency = Counter(alibaba_clean_tokens)
# 确定几何示例的五维词典,首维直接使用实际词元 `ai-driven`。
similarity_vocabulary = ["ai-driven" , "data" , "cloud" , "risk" , "server" ]
# 按确定词典顺序从同一 TF 计数生成阿里巴巴文档向量。
alibaba_document_vector = [alibaba_term_frequency[term] for term in similarity_vocabulary]
# 断言内容词、连字号词元与重复词的计数都与课件说明一致。
assert alibaba_term_frequency["advanced" ] == 1 and alibaba_term_frequency["in-house" ] == 1 and alibaba_term_frequency["technology" ] == 2
# 断言后续五维向量确由确定 unigram 口径生成。
assert alibaba_document_vector == [1 , 1 , 0 , 0 , 0 ]
同源词元结果:从节选到 TF 只计算一次
保留的内容词 :advanced、in-house、ai-driven 都在清洗序列中。
可复算计数 :technology=2、ai-driven=1;五维词典的阿里巴巴向量为 [1, 1, 0, 0, 0]。
清洗序列为:data, center, employ, lead, technology, distribute, fault-tolerant, architecture, advanced, in-house, power, cool, equipment, ai-driven, intelligent, monitor, operational, technology。
后续 TF 表和文档向量只能从这一序列计数,不得另行拆分连字号或替换词元。
BOW第二步:量化 - 词频统计 (Term Frequency)
最简单的量化方法就是统计每个词在文本中出现的频率(Term Frequency, TF)。
以阿里巴巴年报短句为例(经过预处理后),我们可以得到一个词频表:
technology
2
data
1
center
1
architecture
1
equipment
1
ai-driven
1
...
…
这个词频向量是文本最基础的数字化表示: [2, 1, 1, 1, 1, 1, ...]
词频的局限性:所有词都一视同仁吗?
思考一个问题: 在一份年报中,'revenue' 和 'litigation' (诉讼) 都出现了5次。它们的重要性一样吗?
'revenue' (收入) 可能在每一份年报中都会频繁出现,是一个普遍性词汇。
'litigation' (诉讼) 则只在少数面临法律风险的公司年报中出现,是一个特殊性词汇。
直觉告诉我们,稀有的、专业性强的词汇,应该比普遍存在的词汇携带更多的信息量。
改进的量化方法:TF-IDF
TF-IDF (Term Frequency-Inverse Document Frequency) 是一种更智能的词汇权重计算方法,它旨在解决上述问题。
核心思想: 一个词的权重,不仅取决于它在当前文档 中的频率(TF),还取决于它在整个文档集合(语料库) 中的稀有程度(IDF)。
\[
\large{\text{TF-IDF}(t, d, D) = \text{TF}(t, d) \times \text{IDF}(t, D)}
\]
\(t\) : 词汇 (term)
\(d\) : 当前文档 (document)
\(D\) : 语料库 (Document set)
TF-IDF第一部分:原始词频与最终归一化
本章确定为 scikit-learn 默认口径:
\[\operatorname{tf}_{raw}(t,d)=\operatorname{count}(t,d)\]
未归一化权重为 \(w_{t,d}=\operatorname{tf}_{raw}(t,d)\operatorname{idf}_{smooth}(t)\) ,随后每个文档向量默认做 L2 归一化。这里不混用“词频占比”口径。
TF-IDF第二部分:逆文档频率 (IDF)
IDF(t, D): 词 t 在语料库 D 中的逆文档频率。
这衡量了一个词的“稀有度”或“信息量”。
\[\operatorname{idf}_{smooth}(t)=\ln\left(\frac{1+N}{1+df(t)}\right)+1\]
因此出现在全部文档中的词有 idf=1,不会产生负 IDF。
如果一个词在很多 文档中都出现了,分母会很大,IDF值会很低 (如 ‘the’, ‘revenue’)。
如果一个词在极少数 文档中出现,分母会很小,IDF值会很高 (如 ‘litigation’, ‘subpoena’)。
TF-IDF的威力:让关键信息浮出水面
通过将TF和IDF相乘,TF-IDF权重实现了以下效果:
TF-IDF 由 TF 与 IDF 相乘
三张卡片依次解释文档内词频、语料稀有度与最终表示权重。
TF 文档内频次 当前文档证据
×
IDF 语料稀有度 跨文档证据
=
TF-IDF 当前表示权重 不是语义保证
高 TF 且高 IDF → 候选关键词
任一项很低 → 权重降低
演示准备:scikit-learn计算TF-IDF
接下来,我们使用本章选定的 scikit-learn 实现计算 TF-IDF,并把参数口径与手算公式逐项对齐。
我们的语料库 (Corpus):
文档 A: 'company revenue growth is strong' (关于增长)
文档 B: 'the company faces litigation risk' (关于风险)
文档 C: 'company strong growth in product revenue' (关于增长)
我们的目标: 观察 scikit-learn 如何自动识别出每份文档的关键词。
代码演示:使用scikit-learn计算TF-IDF
代码
# 为“代码演示:使用`scikit-learn`计算TF-IDF”,导入 TF–IDF 向量器,把金融文本转换为词项权重矩阵。
from sklearn.feature_extraction.text import TfidfVectorizer
# 为“代码演示:使用`scikit-learn`计算TF-IDF”导入 `pandas` 并绑定 `pd`,用于整理当前任务的表格、字段与时间索引。
import pandas as pd
import numpy as np # 验证默认 L2 归一化与边界词 IDF
# 固定第一段短文本,作为 TF-IDF 文档—词项矩阵的第一行。
doc_A = 'company revenue growth is strong'
# 固定英文诉讼风险句子 `doc_B`,提供只在该文档出现的风险词。
doc_B = 'the company faces litigation risk'
# 固定英文产品收入句子 `doc_C`,与文档 A 共享 growth、strong 和 revenue。
doc_C = 'company strong growth in product revenue'
# 将三段固定短文本组成语料列表,保持文档顺序以解释后续 TF-IDF 矩阵的行索引。
corpus = [doc_A, doc_B, doc_C]
# 建立英文 TF-IDF 向量器并移除英语停用词,固定文档—词项权重口径。
# stop_words='english' 会自动移除英文停用词 (如 'is', 'the', 'in')
vectorizer = TfidfVectorizer(
stop_words= 'english' , use_idf= True , smooth_idf= True ,
sublinear_tf= False , norm= 'l2'
)
# 从三段语料学习词表与 IDF,并生成每篇文档的稀疏 TF-IDF 向量。
# fit: 学习词汇表和IDF权重
# transform: 将文档转换为TF-IDF向量
X = vectorizer.fit_transform(corpus)
# 将稀疏 TF-IDF 矩阵物化为带词项列名和文档行名的教学表格。
df_tfidf = pd.DataFrame(
# 把稀疏文本矩阵转为教学表格可显示的稠密数组。
X.toarray(),
# 指定 `columns` 为输出列名,细化“代码演示:使用scikit-learn计算TF-IDF”的输出。
columns= vectorizer.get_feature_names_out(),
# 用文档 A、B、C 标记三行 TF-IDF 向量,便于对应原始文本。
index= ['文档 A' , '文档 B' , '文档 C' ]
)
# 标出随后展示的是移除停用词后的文档—词项 TF–IDF 权重表。
print ("--- TF-IDF 权重矩阵 (已移除停用词) ---" )
# 输出词项—文档 TF–IDF 表,比较三个金融句子的区分性词权重。
display(df_tfidf.round (2 )) # 展示每篇文档的词频逆文档频率权重
company_idf = vectorizer.idf_[vectorizer.vocabulary_['company' ]] # 取出全语料共同词的平滑逆文档频率
np.testing.assert_allclose(company_idf, 1.0 ) # 验证共同词的默认平滑 IDF 等于一
np.testing.assert_allclose(np.linalg.norm(X.toarray(), axis= 1 ), 1.0 ) # 验证每篇文档向量完成 L2 归一化
top_terms = df_tfidf.apply (lambda row: row.nlargest(2 ).index.tolist(), axis= 1 ) # 提取每篇文档权重最高的两个词
display(top_terms.rename('每篇权重最高的两个词' )) # 核对每篇文档的最高权重词
--- TF-IDF 权重矩阵 (已移除停用词) ---
文档 A
0.41
0.00
0.53
0.00
0.00
0.53
0.00
0.53
文档 B
0.32
0.55
0.00
0.55
0.00
0.00
0.55
0.00
文档 C
0.34
0.00
0.43
0.00
0.57
0.43
0.00
0.43
文档 A [growth, revenue]
文档 B [faces, litigation]
文档 C [product, growth]
Name: 每篇权重最高的两个词, dtype: object
检查:TF-IDF 口径能否复现?
不运行代码,独立写出两行答案:
若词 company 出现在全部三篇文档中,默认平滑 IDF 是多少?
最终文档向量还会经过哪一步?
反馈:用边界词确定实现口径
idf(company)=ln(4/4)+1=1;最终逐文档做 L2 归一化。
订正 :若写成 ln(N/df) 或词频占比,回看 TF 与 IDF 。
课堂路径 :两项正确则进入 余弦相似度 。
课后复现 :代码准备 至 结果解读 不计入 45 分钟课堂核心。
解读TF-IDF结果
数值来源 :代码单元的 df_tfidf.round(2) 与动态 top-2 输出。
实现断言 :company 的平滑 IDF 为 1;逐文档 L2 范数为 1;is/the/in 不进入词表。
解释边界 :高权重只表示当前词表口径下的权重,不自动等同于语义重要性。
BOW第三步:应用 - 基于词典的情感分析
有了量化的词频或TF-IDF权重后,我们就可以进行有意义的金融分析了。最经典的应用就是情感分析 (Sentiment Analysis) 。
情感分析流程
一个流程图,展示文本如何通过情感词典被分类为正面和负面词汇,并最终计算出情感得分。
输入: "Strong growth but risk remains."
情感词典
Pos: strong, growth
Neg: risk, loss, decline
正面词: 2
'strong', 'growth'
负面词: 1
'risk'
情感得分 = 2 - 1 = 1 (正面)
金融领域的情感词典:Loughran-McDonald (LM)
通用情感词典在金融领域往往会失效。
例子: 在日常语境中,'liability' (责任) 可能是个负面词。但在财报中,'liability' (负债) 是一个中性会计术语。
Loughran-McDonald (LM) 词典 针对金融披露语境重新定义情感词类,说明通用词典可能误判“liability”等专业词;其适用性仍需针对中文语料重新标注和验证 (Loughran 和 McDonald 2011年 ) 。
它不仅包含正面/负面 词,还包括对金融市场至关重要的维度:
不确定性 (Uncertainty): e.g., 'approximate', 'contingent'
诉讼 (Litigious): e.g., 'breach', 'fraud', 'subpoena'
强语气 (Strong Modal): e.g., 'always', 'definitely'
弱语气 (Weak Modal): e.g., 'could', 'may', 'possibly'
中文金融情感词典
类似地,中文金融文本分析也需要专门的词典。
来源: 学术界和业界已经构建了一些高质量的中文金融情感词典。
构建方法:
翻译: 将LM词典翻译成中文。
筛选: 从通用的中文情感词典(如HowNet)中筛选出金融相关的词汇。
扩展: 使用Word2Vec等技术,从大规模金融语料(如分析师报告)中自动发现新的情感词。
来源要求: 采用任何中文金融词典前,必须记录作者、年份、题名、版本、许可和词表哈希;当前课件未提供这些信息,因此不把历史作者名或“广泛使用”当作已检查事实。
【可选机制示例|不计达成】待检查的中文金融文本片段
输入边界 :下列片段来自旧课件,尚未定位到年报页码、公告 URL 与文件哈希,不能称为已检查的招商银行 2019 年报原文,也不能作为交易证据。
‘…本集团在持续为客户提供汇率类产品衍生交易服务的同时,发挥在利率互换等金融市场衍生交易方面的专业优势 ,拓展 新的对客衍生交易服务品种,并积极 为客户提供线上交易服务,批发客户数量和交易规模继续保持增长 。…’
任务: 使用一个简化词表演示分词与计数;输出只反映手工词表,不代表公司情绪或未来收益。
招行年报分析步骤1:准备数据与函数
我们将把所有步骤封装到一个代码块中:数据加载、函数定义和执行。
代码
# 导入 jieba,用于把机制文本切分为词元并执行手工情感词表匹配。
import jieba
# 导入标准数学函数接口供课堂扩展;本段可执行的词典计数核心不调用该模块。
import math
# 为“招行年报分析步骤1:准备数据与函数”导入 `pandas` 并绑定 `pd`,用于整理当前任务的表格、字段与时间索引。
import pandas as pd
# 固定正负面手工词表与机制文本,限定词典计数示例的输入边界。
# 这是一个简化的模拟词典,用于教学演示
posi_words = ['优势' , '拓展' , '积极' , '增长' , '机会' , '收益' , '专业优势' ]
# 固定负面词表及词序,作为手工机制示例的可检查匹配口径。
nega_words = ['萎缩' , '风险' , '亏损' , '走势' , '收窄' , '违约' ]
# 招行年报文本
text_cmb = '本集团在持续为客户提供汇率类产品衍生交易服务的同时,发挥在利率互换等金融市场衍生交易方面的专业优势,拓展新的对客衍生交易服务品种,并积极为客户提供线上交易服务,批发客户数量和交易规模继续保持增长。'
# 定义手工词表情感函数,返回正负命中数、净计数和归一化极性。
def sentiment_analysis(text, posi_list, nega_list):
# 用函数文档字符串说明“对输入文本进行简单的情感分析”。
"""对输入文本进行简单的情感分析"""
# 使用jieba分词
tokens = jieba.lcut(text)
# 统计正面和负面词汇的数量
posi_count = sum (1 for word in tokens if word in posi_list)
# 统计分词结果中命中负面词表的次数,作为净计数和极性分母的另一组成项。
nega_count = sum (1 for word in tokens if word in nega_list)
# 计算两种情感得分
# 简单得分
simple_score = posi_count - nega_count
# 情感极性得分,归一化到[-1, 1]之间
total_senti_words = posi_count + nega_count
# 计算 `polarity_score`(评分),量化“招行年报分析步骤1:准备数据与函数”的模型表现。
polarity_score = (posi_count - nega_count) / total_senti_words if total_senti_words > 0 else 0
# 返回正负命中次数、净计数与归一化极性四项指标,供调用方表格化解释。
return {
# 返回正负命中次数与净计数,保留原始词典计数口径。
'正面词数量' : posi_count, '负面词数量' : nega_count,
# 返回净计数与归一化极性,供透明编码检查。
'简单得分' : simple_score, '情感极性' : polarity_score
}
# 对固定机制文本执行词表情感函数,生成可表格化的四项结果。
results = sentiment_analysis(text_cmb, posi_words, nega_words)
# 核对本示例的实际分词边界,防止把连续短语误写成命中词。
cmb_tokens = jieba.lcut(text_cmb)
# 按玩具词典顺序提取实际命中的正面 token。
matched_positive_tokens = [token for token in cmb_tokens if token in posi_words]
# 确定当前 jieba 输出,避免讲解再次把“优势”写成“专业优势”。
assert matched_positive_tokens == ['优势' , '拓展' , '积极' , '增长' ]
# 使用Pandas Series美化输出,并以单次打印核对词典法结果序列。
print ("--- 招商银行年报情感分析结果 --- \n " + pd.Series(results).to_string())
# 输出实际命中词,令讲解直接接受可执行结果检查。
print ({'命中正面词' : matched_positive_tokens})
--- 招商银行年报情感分析结果 ---
正面词数量 4.0
负面词数量 0.0
简单得分 4.0
情感极性 1.0
{'命中正面词': ['优势', '拓展', '积极', '增长']}
解读招行年报分析结果
--- 招商银行年报情感分析结果 ---
正面词数量 4.0
负面词数量 0.0
简单得分 4.0
情感极性 1.0
解读:
在当前 jieba 分词与玩具词典下,实际命中4个正面词(优势、拓展、积极、增长)。
没有匹配到任何负面词。
情感极性 1.0 只表示命中项中 4 正、0 负的归一化编码,不是校准后的情绪强度,也不证明“强正面信息”;分词或词典变化会改变它。
这个计数只能作为待验证候选特征。没有点时文本、训练期词典确定、时间外基线与交易成本检验时,不得据此提出做多或买入建议。
第一部分总结
词袋模型 (BOW) 是将文本转化为数字的基础,它关注词频 而非顺序。
文本预处理 (分词、标准化、去停用词等) 会改变表示与下游结果,须在目标任务上检查。
TF-IDF 在当前语料口径下降权常见词、上调较稀有词;是否优于计数取决于任务、语料、参数和最终测试指标。
词典计数 是透明的候选编码;数值依赖分词、词典与否定处理,须用标注集校准和验证,不能直接当作真实情绪强度。
第二部分:文本相似度计算
核心问题: 如何量化两份文档(例如,两家公司的年报)在内容上的相似程度?
应用场景:
竞争关系候选代理: 文本相似度须用产品市场标签验证,距离本身不能证明两家公司竞争激烈。
IPO 定价候选代理: 须另行定义折价结果、控制信息时点与混杂,并采用明确的识别或预测设计;文本差异不能直接推出信息不对称或折价。
信息抄袭检测。
文本相似度的基础:文档的向量表示
要计算相似度,我们首先需要将每份文档表示为一个向量 (Vector) 。
这个向量就是我们之前通过词袋模型 + 词频/TF-IDF 计算出的结果。
例如 (使用词频): d1 = [count(word1), count(word2), ..., count(wordN)]
假设我们的词典只有5个词,并严格沿用上述确定 unigram 口径:[ai-driven, data, cloud, risk, server]
阿里巴巴年报向量: d_alibaba = [1, 1, 0, 0, 0]
某银行年报向量: d_bank = [0, 0, 1, 2, 1]
首维为 1 是因为词典直接收录 ai-driven;本例没有把它静默拆成 ai 与 driven,也没有做 ai-driven → ai 映射。
我们可以用几何“距离”来衡量相似度
一旦我们将文档表示为向量,计算相似度就变成了一个几何问题 。
我们可以把这些向量想象成高维空间中的点。两个点在空间中的“接近”程度,就反映了对应文档内容的相似度。
主要有两种衡量“接近”程度的方法:
余弦相似度 (Cosine Similarity) - 衡量方向
欧几里得距离 (Euclidean Distance) - 衡量绝对距离
衡量方法1:余弦相似度只描述当前表示
两个非零向量之间夹角的余弦值,就是它们在当前表示空间 中的余弦相似度。
\[
\large{\text{Cosine Similarity} = \cos(\theta) = \frac{\mathbf{d}_i \cdot \mathbf{d}_j}{\|\mathbf{d}_i\| \|\mathbf{d}_j\|}}
\]
取值范围: [0, 1] (因为词频非负)。
1: 只表示两个非零向量同方向,不证明原文或语义相同。
0: 在非负 BOW/TF-IDF 中只表示没有共享正权重维度,不证明语义无关。
任一向量范数为 0 时,余弦未定义。
衡量方法2:欧氏距离为零不等于原文相同
核心思想: 计算两个向量在高维空间中的直线距离。
\[
\large{D(\mathbf{d}_i, \mathbf{d}_j) = \sqrt{\sum_{k=1}^{n} (d_{ik} - d_{jk})^2}}
\]
取值范围: [0, +∞)。
0: 只表示当前向量相同,不证明原文相同。
距离越大, 只表示当前表示下差异更大。
反例: “利润下降风险上升”与“风险下降利润上升”在 unigram BOW 中完全相同,但词序与语义不同;空词表或全停用词还会产生零向量。
检查:表示相同能否推出语义相同?
若两句 unigram BOW 的欧氏距离为 0,能否断言原文或语义相同?若余弦计算遇到零向量,应如何报告?
反馈:先限定表示层,再解释端点
不能。距离端点只约束所选表示;零向量的余弦应报告“未定义”,并检查空词表、停用词和分词流程。
余弦相似度 vs. 欧几里得距离
余弦相似度与欧几里得距离的对比
一个二维向量空间图,展示了三个向量D1, D2, D3,并图示了它们之间的余弦相似度(角度)和欧几里得距离(直线),强调余弦相似度关注方向,而欧几里得距离关注长度。
“科技”词频
“金融”词频
D1 (短篇)
D2 (长篇)
D3 (金融)
θ₁₂
余弦比较方向
cos(θ₁₂) > cos(θ₁₃)
欧氏距离比较位置
受向量模长差异影响
结论: 若任务强调非负词频向量的方向且希望降低模长影响,余弦相似度是候选;欧氏距离或任务学习度量仍须在同一评估集比较。
第二部分总结
要计算文本相似度,首先要将文本用词袋模型 转化为向量 。
余弦相似度 通过向量夹角比较方向,对整体正比例缩放不变;它不是语义相似的自动保证。
欧几里得距离 计算向量在高维空间中的绝对距离 ,对文档长度敏感。
文本相似度只形成候选测量;竞争或信息不对称的经济解释须另有标签、基准与识别或样本外验证。
第三部分:超越词袋 - 词嵌入 (Word Embedding)
词袋模型的根本缺陷:
每个词都被视为独立、孤立的单元。
'good'、'excellent' 与 'terrible' 只是三个不同维度。
BOW 本身不编码这些词之间的语义远近。
BOW无法理解词汇之间的语义关系。
词嵌入的核心思想:从上下文学习词义
词嵌入模型基于一个著名的语言学假设——分布假说 (Distributional Hypothesis):
“You shall know a word by the company it keeps.” (J.R. Firth, 1957)
(你可以通过一个词的上下文来理解它的意义)
核心思想: 意义相近的词,其上下文也往往是相似的。
例如,'dog' 和 'cat' 经常出现在 pet, food, vet 等词的附近。
而 'dog' 和 'algorithm' 的上下文则几乎没有交集。
什么是词嵌入?
词嵌入 (Word Embedding) 是一种技术,它将每个单词映射到一个低维、稠密的实数向量 上。这个向量就被称为该单词的“词向量”。
从One-Hot到词嵌入
图示高维稀疏的词项身份编码转为低维稠密的分布表示;邻近关系来自训练语料共现,不构成语义保证。
词袋模型 (One-Hot)
[0, 0, ..., 1, ..., 0, 0]
高维 (e.g., 50,000维)
稀疏 (大部分是0)
未编码分布关联
Embedding
词嵌入 (Dense Vector)
[0.26, -0.41, 0.55, ...]
低维 (e.g., 300维)
稠密 (都是实数)
学习语料共现关联
关键边界 :静态嵌入可编码训练语料的分布规律;向量邻近是否对应任务所需语义须验证,也可能反映语料偏差。
著名词嵌入模型:Word2Vec
Word2Vec 是 Google 在2013年提出的一个项目阶段式的词嵌入模型。它通过一个简单的神经网络,从海量文本数据中学习词向量。
它主要包含两种架构:
CBOW (Continuous Bag-of-Words): 根据上下文词,预测中心词。 (类似完形填空)
Skip-gram: 根据中心词,预测上下文词。
我们以CBOW为例来理解其工作原理。
Word2Vec (CBOW) 工作原理:一场“猜词游戏”
Word2Vec CBOW 模型结构
一个简化的神经网络图,展示了CBOW如何使用上下文词向量(输入)来预测中心词(输出),中间的隐藏层即为学习到的词向量。
输入 (上下文词)
... increase
our
costs
and ...
隐藏层 (学习词向量)
Embedding
输出 (预测中心词)
operating
growth
demand
模型通过海量猜词,学习出能最好完成任务的词向量 (隐藏层权重)。
词向量类比是可检验且可能失败的现象
在某些已训练静态嵌入及特定检索口径下,King − Man + Woman ≈ Queen 可能出现,也可能失败,并可能编码语料偏差;向量运算不等于语义运算 。
待检验的词向量类比现象
向量平行四边形仅示意 King - Man + Woman 可能接近 Queen;结果依赖已训练嵌入与检索口径,也可能失败或编码偏差。
Man
Woman
King
Queen
Gender Vector
Royalty Vector
King - Man + Woman ≈ Queen
金融向量关系是待验证假设
本章没有执行金融词向量类比。任何向量关系都依赖模型、训练语料、tokenization 与检索口径,必须确定这些条件并用事先确定 top-k 检索和人工标签检查,不能把示意算式当作事实或金融知识。
词嵌入的金融应用远超情感分析
使用词嵌入,我们可以构建更复杂的模型:
候选情感表示 :种子词距离可形成特征,但是否优于词典或字符 n-gram TF-IDF 必须在同一时间外切分上比较。
候选文本相似度 :平均词向量会丢失词序和频次结构,其增量由确定语料、任务与指标验证。
主题变化假设 :历年文档表示的漂移可提出战略变化假设,但需人工证据与版本稳定性检查。
预测用途 :任何波动率或收益任务都须明确点时文本、目标期、基线、成本与最终测试指标;本章未执行此类任务。
总结:从词袋到词嵌入的认知升级
核心思想
统计词频,词项彼此分列
静态词向量从训练语料的分布上下文学习关联
向量表示
高维、稀疏
低维、稠密
可编码关系
词项共现与频次
训练语料中的分布关系,需任务验证
优点
简单、快速、可解释性强
可形成低维稠密的候选表示
缺点
忽略词序与上下文
固定 Word2Vec 中同一 token 只有一个向量,不随当前句子变化;语料偏差、OOV/tokenization 与迁移风险
上下文化表示可随当前输入语境改变 token 表示;这与 Word2Vec 等静态词向量必须明确区分。
结论 :本章可执行证据仅建立字符 n-gram TF-IDF 对下一财年审计意见标签的时间外评估;未比较词嵌入,未证明语义理解、未来收益或 alpha。
从公开数据到可重复的文本分析
本章使用公开的审计意见数据。读者与作者都从同一个地址下载,并在当前 QMD 中完成读取、建模和结果核对。
公开数据的学习流程
依次展示公开下载、当前文档读取、运行文本分析和核对结果四个步骤。
1. 公开下载
同一份审计意见数据
无需另找来源
2. 文档读取
在当前 QMD 中
查看字段与样本
3. 运行分析
TF-IDF 与分类模型
按时间划分样本
4. 核对
比较基线
解释结果
阶段小结:从词袋到词嵌入
今天,我们踏上了一段将语言转化为数字的旅程。
我们从最简单的词袋模型 出发,学会了如何预处理 文本,并通过TF-IDF 和情感词典 进行量化分析。
我们掌握了如何使用余弦相似度 形成当前确定文本表示下的向量相似度候选代理;它不能直接证明公司战略相似,经济解释仍需外部标签与样本外验证。
最后,我们把词嵌入视为依赖语料与任务的候选表示;是否优于计数表示必须另行验证。
本章的实证达成限于字符 n-gram TF-IDF、下一财年审计意见标签与指定扩展窗口;结构化字段不等于公告正文语义,也不构成因果或交易证据。
字符 n-gram:不分词也能形成局部片段
2-gram :“审计、计意、意见”。
3-gram :“审计意、计意见”。
折内约束 :词表、文档频率和分类器只在当前训练折拟合,再转换较晚验证年。
边界 :字符 n-gram 表示不保证语义理解。
检查:折内拟合发生在哪里?
若先对 2020—2022 全部文本 fit_transform,再切扩展窗口,是否无泄漏?一个验证年出现的新字符片段应怎样处理?
反馈:验证文本只能调用 transform
错误 :全量 fit_transform 让验证文本参与词表与 IDF 估计,形成泄漏。
正确 :每个训练折内拟合 TfidfVectorizer(analyzer='char', ngram_range=(2,3))。
验证年 :只调用 transform;未见片段按既定词表忽略。
订正 :回看 TF 、IDF 与本页最小例。
独立练习:文本信号的时间外检查
固定输入 :stock/audit_opinion.h5 的 audit_opinion 键;每条记录带 info_date。
文本与标签 :文本只含当期审计机构和意见类型;标签为下一财年是否收到非标准审计意见。
任务边界 :这不是公告全文情感或下一交易日收益任务,只验证 TF-IDF + 线性分类流程。
独立练习:提交要求
提交要求
写明 point-in-time 顺序:特征财年 \(y\) 、预测截止 \(y+1\) 年 6 月 30 日、标签财年 \(y+1\) 、评分截止 \(y+2\) 年 6 月 30 日。
在 2020—2022 三个扩展窗内,将字符 2—3 gram TF-IDF 与分类器绑定为只在训练折拟合的 Pipeline。
逐年提交模型与训练折多数类的 balanced accuracy、固定 labels=[0,1] 的混淆矩阵,以及至少一个具名误分类。
说明结构化机构/意见字段不等于正文语义,结果也不是因果或交易证据。
练习:先完成再查看答案:文本流程先最终测试
提交内容 :PIT 文本—标签顺序、折内 TF-IDF Pipeline、逐年结果、多数类基线、混淆矩阵与三条误分类检查。
失败处理 :明确写出“暂不采用”及原因。
完成后 :再进入答案页。
完整答案(1/3):固定文本、日期与下一财年标签
代码
from pathlib import Path # 管理公开审计公告结构化数据版本
from urllib.request import urlretrieve # 复用本章隐藏设置单元安装的浏览器标识下载器
import hashlib # 生成固定文本夹具的内容哈希
import pandas as pd # 构造公司财年文本面板
# 按 Linux 共享数据、Windows 共享数据、项目缓存的顺序选择审计意见文件。
audit_text_path = next ((candidate_path for candidate_path in [Path('/home/ubuntu/r2_data_mount/data/stock/audit_opinion.h5' ), Path('C:/qiufei/data/stock/audit_opinion.h5' ), Path('data/course/audit_opinion.h5' )] if candidate_path.exists()), Path('data/course/audit_opinion.h5' ))
if not audit_text_path.exists():
audit_text_path.parent.mkdir(parents= True , exist_ok= True )
urlretrieve('https://assets.qiufei.site/data/stock/audit_opinion.h5' , audit_text_path)
audit_raw = pd.read_hdf(audit_text_path, key= 'audit_opinion' ) # 读取并保留全部披露版本
audit_raw['info_date' ] = pd.to_datetime(audit_raw['info_date' ]) # 统一公告日期类型
audit_raw = audit_raw.query("type == 'financial_statements' and quarter.str.endswith('q4')" , engine= 'python' ).copy() # 仅保留年度财报审计公告
audit_raw['year' ] = audit_raw['quarter' ].str [:4 ].astype(int ) # 提取当期财年
opinion_words = {'unqualified' : '标准无保留意见' , 'unqualified_with_explanation' : '带说明段无保留意见' , 'qualified' : '保留意见' , 'disclaimer' : '无法表示意见' , 'adverse' : '否定意见' } # 固定意见类型中文映射
def audit_asof(fiscal_year, cutoff): # 每个截止日从原始版本重建
all_versions = audit_raw.loc[audit_raw['year' ].eq(fiscal_year)].copy() # 保留该财年全部版本
eligible = all_versions.loc[all_versions['info_date' ].le(cutoff)].copy() # 先按可得日过滤
if eligible.empty: # 空窗口显式失败
raise RuntimeError (f' { fiscal_year} @ { cutoff. date()} 空窗' ) # 点时样本缺失时立即停止而不回填未来文本
snap = eligible.sort_values(['order_book_id' , 'year' , 'info_date' ]).drop_duplicates(['order_book_id' , 'year' ], keep= 'last' ) # 再选当时最后版本
flow = {'raw' : len (all_versions), 'eligible' : len (eligible), 'excluded' : len (all_versions) - len (eligible), 'companies' : snap.order_book_id.nunique()} # 样本数量变化
return snap, flow # 返回点时数据版本
代码
def build_audit_year(year): # 构造当期文本与下一财年标签
prediction_cutoff, scoring_cutoff = pd.Timestamp(f' { year + 1 } -06-30' ), pd.Timestamp(f' { year + 2 } -06-30' ) # 确定双截止日
features, feature_flow = audit_asof(year, prediction_cutoff) # 预测日文本版本
label_all = audit_raw.loc[audit_raw['year' ].eq(year + 1 )].copy() # 标签版本先不去重
assert len (label_all) > 0 and label_all.info_date.min () > prediction_cutoff # 目标不得提前可见
labels, label_flow = audit_asof(year + 1 , scoring_cutoff) # 评分日标签版本
features['document' ] = '审计机构 ' + features.audit_agency.fillna('未披露机构' ) + ' 审计意见 ' + features.opinion_type.map (opinion_words).fillna('其他意见' ) # 仅用特征数据版本构文档
labels = labels[['order_book_id' , 'info_date' , 'opinion_type' ]].rename(columns= {'info_date' : 'label_info_date' }) # 保留标签披露日
labels['next_modified' ] = labels.opinion_type.ne('unqualified' ).astype(int ) # 构造下一年标签
panel = features.rename(columns= {'info_date' : 'feature_info_date' }).merge(labels, on= 'order_book_id' ).assign(prediction_cutoff= prediction_cutoff, scoring_cutoff= scoring_cutoff, next_year= year + 1 ) # 合并点时样本
assert len (panel) > 0 and panel.feature_info_date.le(prediction_cutoff).all () and panel.label_info_date.gt(prediction_cutoff).all () and panel.label_info_date.le(scoring_cutoff).all () # 日期不变量
return panel, {'year' : year, 'feature' : feature_flow, 'label' : label_flow, 'merged' : len (panel)} # 返回面板与样本数量变化
代码
audit_parts, audit_flows = [], [] # 收集逐年点时样本
for fiscal_year in range (2016 , 2023 ): # 固定建模财年
fiscal_panel, fiscal_flow = build_audit_year(fiscal_year) # 从原始版本重建
audit_parts.append(fiscal_panel) # 保存样本
audit_flows.append(fiscal_flow) # 保存检查结果
audit_text_panel = pd.concat(audit_parts, ignore_index= True ) # 合并点时面板
assert not audit_text_panel.duplicated(['order_book_id' , 'year' ]).any () # 公司财年唯一
assert audit_text_panel.feature_info_date.lt(audit_text_panel.label_info_date).all () # 特征严格早于标签
fixture_hash = hashlib.sha256(' \n ' .join(audit_text_panel['document' ]).encode('utf-8' )).hexdigest() # 记录文档顺序与内容哈希
print ({'文件' : audit_text_path.name, '键' : 'audit_opinion' , '字段' : ['info_date' , 'audit_agency' , 'opinion_type' ], '样本期' : (audit_text_panel.year.min (), audit_text_panel.year.max ()), '文档数' : len (audit_text_panel), 'sha256' : fixture_hash}) # 输出输入说明
display(pd.DataFrame(audit_flows)) # 展示每年版本选择与排除数
{'文件': 'audit_opinion.h5', '键': 'audit_opinion', '字段': ['info_date', 'audit_agency', 'opinion_type'], '样本期': (2016, 2022), '文档数': 28741, 'sha256': 'ce462ac7a1d29e3809d7420289e940f04e4bd934a5625b87e71edacadc8d3a29'}
0
2016
{'raw': 4701, 'eligible': 3597, 'excluded': 11...
{'raw': 5095, 'eligible': 3760, 'excluded': 13...
3445
1
2017
{'raw': 5095, 'eligible': 3760, 'excluded': 13...
{'raw': 5367, 'eligible': 3835, 'excluded': 15...
3646
2
2018
{'raw': 5367, 'eligible': 3835, 'excluded': 15...
{'raw': 5414, 'eligible': 3951, 'excluded': 14...
3687
3
2019
{'raw': 5414, 'eligible': 3951, 'excluded': 14...
{'raw': 5425, 'eligible': 4409, 'excluded': 10...
3856
4
2020
{'raw': 5425, 'eligible': 4409, 'excluded': 10...
{'raw': 5419, 'eligible': 4747, 'excluded': 67...
4337
5
2021
{'raw': 5419, 'eligible': 4747, 'excluded': 67...
{'raw': 5439, 'eligible': 5101, 'excluded': 33...
4708
6
2022
{'raw': 5439, 'eligible': 5101, 'excluded': 33...
{'raw': 5402, 'eligible': 5228, 'excluded': 17...
5062
完整答案(2/3):逐年扩展窗口 Pipeline
代码
from sklearn.feature_extraction.text import TfidfVectorizer # 构造中文字符级文本表示
from sklearn.linear_model import LogisticRegression # 建立线性文本分类器
from sklearn.metrics import balanced_accuracy_score, confusion_matrix # 报告类别均衡指标与错误类型
from sklearn.pipeline import Pipeline # 确保词表、IDF和分类器只在训练折拟合
import numpy as np # 构造训练期多数类预测
evaluation_rows = [] # 收集逐年扩展窗口结果
misclassified_rows = [] # 收集可供反馈的误分类记录
for validation_year in [2020 , 2021 , 2022 ]: # 按预设三个年份扩展验证
fit_cutoff = pd.Timestamp(f' { validation_year + 1 } -06-30' ) # 确定验证年预测日
training_text = audit_text_panel.query('year < @validation_year and label_info_date <= @fit_cutoff' ) # 只用拟合日已评分历史标签
validation_text = audit_text_panel.query('year == @validation_year' ) # 固定当年结构化公告文本
if training_text.empty or validation_text.empty or training_text.next_modified.nunique() < 2 or validation_text.next_modified.nunique() < 2 : # 空窗或单类显式失败
raise RuntimeError (f' { validation_year} 点时窗口为空或单类' ) # 单类窗口无法形成有效分类验证
text_pipeline = Pipeline([('tfidf' , TfidfVectorizer(analyzer= 'char' , ngram_range= (2 , 3 ), min_df= 3 , max_features= 1000 )), ('classifier' , LogisticRegression(max_iter= 500 , class_weight= 'balanced' , random_state= 42 ))]) # 折内绑定文本表示与线性分类器
text_pipeline.fit(training_text['document' ], training_text['next_modified' ]) # 只在当前扩展训练窗估计词表与权重
validation_prediction = text_pipeline.predict(validation_text['document' ]) # 生成下一财年非标准意见预测
majority_label = int (training_text['next_modified' ].mode().iloc[0 ]) # 仅由训练折确定多数类
majority_prediction = np.repeat(majority_label, len (validation_text)) # 建立同窗多数类基线
fold_matrix = confusion_matrix(validation_text['next_modified' ], validation_prediction, labels= [0 , 1 ]) # 固定二乘二错误矩阵
assert fold_matrix.shape == (2 , 2 ) and training_text.label_info_date.le(fit_cutoff).all () and validation_text.feature_info_date.le(fit_cutoff).all () and validation_text.label_info_date.gt(fit_cutoff).all () # 确定折内日期与矩阵不变量
evaluation_rows.append({'year' : validation_year, 'fit_cutoff' : fit_cutoff.date(), 'train_n' : len (training_text), 'valid_n' : len (validation_text), 'train_classes' : training_text.next_modified.value_counts().to_dict(), 'valid_classes' : validation_text.next_modified.value_counts().to_dict(), 'model_balanced_accuracy' : balanced_accuracy_score(validation_text['next_modified' ], validation_prediction), 'majority_balanced_accuracy' : balanced_accuracy_score(validation_text['next_modified' ], majority_prediction), 'confusion_matrix' : fold_matrix.tolist()}) # 保存年度指标与类别流
errors = validation_text.loc[validation_prediction != validation_text['next_modified' ], ['order_book_id' , 'year' , 'feature_info_date' , 'label_info_date' , 'document' , 'next_modified' ]].copy() # 定位具名公司财年错误案例
errors['prediction' ] = validation_prediction[validation_prediction != validation_text['next_modified' ].to_numpy()] # 附加错误预测类别
misclassified_rows.append(errors.head(1 )) # 每年最多保留一个具体误分类
text_evaluation = pd.DataFrame(evaluation_rows) # 汇总年度稳定性结果
完整答案(3/3):基线、稳定性与错误反馈
代码
display(text_evaluation[['year' , 'train_n' , 'valid_n' , 'model_balanced_accuracy' , 'majority_balanced_accuracy' ]].round (4 )) # 展示紧凑逐年指标
print ({'逐年混淆矩阵[0,1]' : dict (zip (text_evaluation.year, text_evaluation.confusion_matrix))}) # 另行保留固定标签顺序的错误流
text_errors = pd.concat(misclassified_rows, ignore_index= True ) # 合并逐年代表性误分类
stable_improvement = bool ((text_evaluation['model_balanced_accuracy' ] > text_evaluation['majority_balanced_accuracy' ]).all ()) # 检查是否每年都胜多数类
print ({'每年均胜多数类' : stable_improvement, '结论' : '仅限下一财年审计意见风险' if stable_improvement else '证据不稳定,不宣称增量价值' }) # 给出与实际结果绑定的反馈
{'逐年混淆矩阵[0,1]': {2020: [[3843, 246], [62, 186]], 2021: [[4215, 231], [64, 198]], 2022: [[4556, 234], [75, 197]]}}
{'每年均胜多数类': True, '结论': '仅限下一财年审计意见风险'}
表 1: 公开检查结构化文本的逐年扩展窗口分类结果
0
2020
14634
4337
0.8449
0.5
1
2021
18971
4708
0.8519
0.5
2
2022
23679
5062
0.8377
0.5
错误反馈:逐年代表性误分类
代码
display(text_errors[['order_book_id' , 'year' , 'document' , 'next_modified' , 'prediction' ]].rename(columns= {
'order_book_id' : '证券代码' , 'year' : '财年' , 'document' : '文本' ,
'next_modified' : '实际类别' , 'prediction' : '预测类别'
}).head(3 ))
表 2: 公开检查结构化文本的代表性时间外误分类
0
000004.XSHE
2020
审计机构 大华会计师事务所(特殊普通合伙) 审计意见 标准无保留意见
1
0
1
000004.XSHE
2021
审计机构 致同会计师事务所(特殊普通合伙) 审计意见 其他意见
0
1
2
000003.XSHE
2022
审计机构 深圳久安会计师事务所(特殊普通合伙) 审计意见 标准无保留意见
0
1
完整反馈 :词表折内拟合;报告逐年指标、基线、固定标签顺序混淆矩阵与具名误分类。结构化机构/意见字段不等于正文语义;归因非因果,结果也不是交易证据。
本章小结
学完后应能 :能复现默认 TF-IDF、折内字符 n-gram 流程、扩展窗与多数类基线,并解释一个错误案例。
选择条件 :只有字段、info_date、标签期和切分说明均确定,时间外指标才可解释。
未建立 :结构化审计字段不等于公告正文;本章没有证明情感、因果或可交易性。
下一站 :进入第 16 章,用同样的证据说明训练检索—引用—拒答边界。