15 基础文本分析

本章会用到的数据

  • 公开下载上市公司审计意见。本章使用披露日期、审计机构和意见类型等结构化字段。

  • 这些数据能做什么:练习中文分词、词袋模型和 TF-IDF 分类。

  • 分析时注意:本章分析的是结构化字段,不等同于对完整年报正文做情感分析。

  • 判断模型:用较早年份训练、下一年验证,最后在未参与选择的年份检查一次。

【课堂核心】3 学时学习安排

学习内容 分钟
动机与先修 20
BOW、TF-IDF 检查与反馈 45
相似度边界与检查 40
独立练习 55
反馈与小结 20

【可选拓展】:除学习安排已列出的阿里巴巴短引文与正式引用外,其他正文片段、情感词典、词嵌入和外部工具留作课后阅读,不属于本章学习要求。

先修入口检查:向量、折与标签

限时 6 分钟,独立提交三行:

  1. 向量、L2 范数与余弦相似度的含义。
  2. 为何词表、IDF 与缩放必须在每个训练折拟合。
  3. 文本 info_date、决策日与下一财年标签的先后顺序。

提交前不看下一页。

先修反馈与即时订正

  • 向量层:向量是有序特征权重,L2 范数表示长度,余弦衡量方向。
  • 验证层:词表或 IDF 若读到验证、测试文本,就会产生信息泄漏。
  • 时间层:先取得决策日前文本,再在未来可得日生成标签。
  • 即时订正:概念漏项回看向量范数页;折内漏项回看第 6 章;时点倒置先重画时间线。

欢迎来到文本分析的世界

从财报到候选文本信号

财报语言可以被量化为可检验的文本特征,但是否具有样本外预测力必须在明确的信息时点、交易成本和最终测试期下验证。本章不再保留来源不明的“5% 超额收益”数字。

语言,不再仅仅是描述,它本身就是数据。

从文本到信号 一个文档图标通过量化分析转化为待检验候选文本特征;图形不代表已实现 alpha 或投资收益。 10-K 量化分析 文本信号

本章学习目标:核心问题

核心问题: 公司的年报、新闻稿和分析师报告中充满了重要的非结构化信息。我们如何系统性地、大规模地将这些文本转化为可用于金融建模的量化信号?

本章学习目标:能力养成

本章结束后,你将能够:

  1. 解释 BOW 与 TF-IDF 的精确计算口径。
  2. 构造 折内拟合的中文字符 n-gram 分类流程。
  3. 比较 扩展窗口模型与多数类基线,并解释混淆矩阵。
  4. 诊断 零向量、词序丢失与时间泄漏等结果不理想时如何解释。
  5. 说明 本章使用审计机构与意见类型构造的结构化字符串,未完成真实公告正文情感分析。
本章能力目标图示 五个图标,分别代表情感分析、文本预处理、相似度计算、词嵌入和编程实现。 😃/😠 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…’

结构化数据

  • 数值矩阵
  • 格式统一
  • 干净整洁
  • 可直接用于模型
增长 风险 宏观
0.85 0.92 0.51

本章任务: 我们将学习如何搭建一座桥梁,将左边的非结构化文本,转化为右边的结构化特征向量(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

为了让模型知道它们都指向同一个核心概念“投资”,我们需要将它们标准化为基本形式。

两种主要方法:

  1. 词干提取 (Stemming): 按规则截断,速度快,但可能产生非词。
  2. 词形还原 (Lemmatization): 借助词典、形态与词性映射返回 lemma;质量取决于语言、词典覆盖、词性与领域词汇,不保证更准确。

方法一:词干提取 (Stemming)

词干提取是一种比较粗糙的标准化方法,它通过移除单词的后缀(有时是前缀)来将其简化为词干(stem)。

  • 特点: 速度快,计算开销小,不依赖词典。
  • 缺点: 结果不一定是真实的单词。

例如:

  • investmentsinvest
  • makingmake
  • operatingoper (注意:oper 不是一个有效的英文单词)
  • studiesstudi (同样不是有效单词)

方法二:词形还原 (Lemmatization)

词形还原利用词汇、形态与词性信息,将单词映射到词典基本形式(lemma)。在资源覆盖且词性正确时,结果通常更可读;未知词、金融术语和词性错误仍会导致失败。

  • 特点: 输出取决于目标语言、词典覆盖和词性标注。
  • 缺点: 资源依赖与计算开销更大,不能保证下游任务更准确。

例如:

  • investmentsinvestment
  • makingmake
  • operatingoperate
  • studiesstudy
  • bettergood (词干提取无法处理这种情况)

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.'(否定增长命题;极性仍依赖语境与标注规则)

简单的停用词删除可能丢失 nonot 等否定信息;将 'not good' 合并为 'not_good' 只是一种候选编码,须与保留原词序等方案在同一任务上比较。

确定词元说明:清洗表与向量必须同源

  • Tokenizer:用 [A-Za-z]+(?:-[A-Za-z]+)* 取词,保留词内连字号;因此 AI-drivenfault-tolerantin-house 各是一个 unigram。
  • 大小写与停用词:先转小写;本句仅删除确定集合 {our, in, and}
  • 词形映射centers→centerleading→leadtechnologies→technologydistributed→distributecooling→coolmonitoring→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 只计算一次

  • 保留的内容词advancedin-houseai-driven 都在清洗序列中。
  • 可复算计数technology=2ai-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)。

以阿里巴巴年报短句为例(经过预处理后),我们可以得到一个词频表:

单词 (Term) 词频 (Frequency)
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 权重矩阵 (已移除停用词) ---
company faces growth litigation product revenue risk strong
文档 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 口径能否复现?

不运行代码,独立写出两行答案:

  1. 若词 company 出现在全部三篇文档中,默认平滑 IDF 是多少?
  2. 最终文档向量还会经过哪一步?

反馈:用边界词确定实现口径

  • idf(company)=ln(4/4)+1=1;最终逐文档做 L2 归一化。
  • 订正:若写成 ln(N/df) 或词频占比,回看 TFIDF
  • 课堂路径:两项正确则进入 余弦相似度
  • 课后复现代码准备结果解读 不计入 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'

中文金融情感词典

类似地,中文金融文本分析也需要专门的词典。

  • 来源: 学术界和业界已经构建了一些高质量的中文金融情感词典。
  • 构建方法:
    1. 翻译: 将LM词典翻译成中文。
    2. 筛选: 从通用的中文情感词典(如HowNet)中筛选出金融相关的词汇。
    3. 扩展: 使用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;本例没有把它静默拆成 aidriven,也没有做 ai-driven → ai 映射。

我们可以用几何“距离”来衡量相似度

一旦我们将文档表示为向量,计算相似度就变成了一个几何问题

我们可以把这些向量想象成高维空间中的点。两个点在空间中的“接近”程度,就反映了对应文档内容的相似度。

主要有两种衡量“接近”程度的方法:

  1. 余弦相似度 (Cosine Similarity) - 衡量方向
  2. 欧几里得距离 (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年提出的一个项目阶段式的词嵌入模型。它通过一个简单的神经网络,从海量文本数据中学习词向量。

它主要包含两种架构:

  1. CBOW (Continuous Bag-of-Words): 根据上下文词,预测中心词。 (类似完形填空)
  2. 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 检索和人工标签检查,不能把示意算式当作事实或金融知识。

词嵌入的金融应用远超情感分析

使用词嵌入,我们可以构建更复杂的模型:

  1. 候选情感表示:种子词距离可形成特征,但是否优于词典或字符 n-gram TF-IDF 必须在同一时间外切分上比较。
  2. 候选文本相似度:平均词向量会丢失词序和频次结构,其增量由确定语料、任务与指标验证。
  3. 主题变化假设:历年文档表示的漂移可提出战略变化假设,但需人工证据与版本稳定性检查。
  4. 预测用途:任何波动率或收益任务都须明确点时文本、目标期、基线、成本与最终测试指标;本章未执行此类任务。

总结:从词袋到词嵌入的认知升级

特性 词袋模型 (BOW) 词嵌入 (Embedding)
核心思想 统计词频,词项彼此分列 静态词向量从训练语料的分布上下文学习关联
向量表示 高维、稀疏 低维、稠密
可编码关系 词项共现与频次 训练语料中的分布关系,需任务验证
优点 简单、快速、可解释性强 可形成低维稠密的候选表示
缺点 忽略词序与上下文 固定 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;未见片段按既定词表忽略。
  • 订正:回看 TFIDF 与本页最小例。

独立练习:文本信号的时间外检查

  • 固定输入stock/audit_opinion.h5audit_opinion 键;每条记录带 info_date
  • 文本与标签:文本只含当期审计机构和意见类型;标签为下一财年是否收到非标准审计意见。
  • 任务边界:这不是公告全文情感或下一交易日收益任务,只验证 TF-IDF + 线性分类流程。

独立练习:提交要求

提交要求

  1. 写明 point-in-time 顺序:特征财年 \(y\)、预测截止 \(y+1\) 年 6 月 30 日、标签财年 \(y+1\)、评分截止 \(y+2\) 年 6 月 30 日。
  2. 在 2020—2022 三个扩展窗内,将字符 2—3 gram TF-IDF 与分类器绑定为只在训练折拟合的 Pipeline
  3. 逐年提交模型与训练折多数类的 balanced accuracy、固定 labels=[0,1] 的混淆矩阵,以及至少一个具名误分类。
  4. 说明结构化机构/意见字段不等于正文语义,结果也不是因果或交易证据。

练习:先完成再查看答案:文本流程先最终测试

  • 提交内容: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'}
year feature label merged
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: 公开检查结构化文本的逐年扩展窗口分类结果
year train_n valid_n model_balanced_accuracy majority_balanced_accuracy
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 章,用同样的证据说明训练检索—引用—拒答边界。

Q&A

有问题吗?

参考文献

Loughran, Tim, 和 Bill McDonald. 2011年. 《When Is a Liability Not a Liability? Textual Analysis, Dictionaries, and 10-Ks》. The Journal of Finance 66 (1): 35~65. https://doi.org/10.1111/j.1540-6261.2010.01625.x.