import pandas as pd
# 读取人民银行新闻数据
# 注意:该文件编码为 GBK(Windows 中文系统常见编码),不是 UTF-8
df = pd.read_csv("data_raw/pbc_news_raw.csv", encoding="gbk")
print(f"数据维度:{df.shape[0]} 行 × {df.shape[1]} 列")
print(f"字段名称:{df.columns.tolist()}")25 文本数据处理基础
金融市场有一个有趣的特点:很多重要信息,是先以文字的形式出现,再转化为价格变动的。央行一句措辞的变化、一份公告的标题、一篇年报里某个段落的语气——这些文本信号,往往比数字数据更早、也更直接地反映了市场参与者的预期和情绪。
以人民银行为例。2021年底,货币政策执行报告中将「流动性合理充裕」改为「流动性保持合理充裕」,一字之差,市场解读为政策边际宽松;2023年底,「稳健的货币政策要灵活适度、精准有效」中「灵活适度」的措辞出现,债市随即上涨。这些变化,如果只盯着利率数据是看不到的——它们首先藏在文字里。
本章要做的事情,就是教会你把这些文字变成可以分析的数据。主线流程如下:
\[ \text{读取文本} \rightarrow \text{检查问题} \rightarrow \text{清洗文本} \rightarrow \text{分词} \rightarrow \text{词频统计} \rightarrow \text{TF-IDF} \]
这一章只处理「文本变成数据」这件事,不涉及情感分析、主题模型等更进一步的应用——那些内容留到下一章。
学完本章,你应该能够:
- 把原始文本整理成规则表格
- 对文本做基本检查和清洗
- 使用 jieba 对中文文本分词,并加载自定义词典
- 统计高频词,生成词云和柱状图
- 理解 TF-IDF 的直觉,并用 scikit-learn 实现
与前两章的衔接:在「数据清洗」章,我们已经初步接触了非结构化文本——从贷款公告中提取金额、利率等结构化字段,那是「从文本中抽取事实」。本章要做的事更进一步:量化文本本身的特征,让文本也成为可分析的变量。
与下一章的衔接:本章使用的数据,是已经整理好的人民银行新闻和问答文本。如果你想自己去采集更多文本,下一章「网络爬虫」会教你怎么做。
本章各节概览
| 节 | 内容 | 核心工具 |
|---|---|---|
| 文本数据从哪里来,怎么整理成表格 | pandas | |
| 读取数据后怎么做初步检查 | pandas | |
| 文本清洗:去噪、标准化与去重 | re, pandas | |
| 中文分词与停用词处理 | jieba | |
| 高频词统计与可视化 | collections, wordcloud, matplotlib | |
| TF-IDF:找出真正有区分度的词 | scikit-learn | |
| 本章小结与练习 | — |
25.1 文本数据从哪里来,怎么整理成表格
25.1.1 金融文本的常见来源
在实际工作中,金融文本数据大致来自以下几类来源:
| 类型 | 典型来源 | 适合做什么 |
|---|---|---|
| 政策文件与问答 | 人民银行官网、国务院新闻办 | 政策语气分析、关键词追踪 |
| 上市公司公告 | 巨潮资讯、深交所、上交所 | 公告分类、事件识别 |
| 年报与招股说明书 | 巨潮资讯、证监会 | 风险词频、MD&A 分析 |
| 财经新闻 | 新华财经、中证网、Wind | 情绪分析、事件检测 |
| 投资者问答 | 深交所互动易、上证 e 互动 | 投资者关注点分析 |
本章使用的数据来自人民银行官网,包含两类文本:
pbc_news_raw.csv:人民银行新闻(2025 年 11 月至 2026 年 3 月,共 120 条)pbc_qa_raw.csv:人民银行答记者问(共 185 条,包含完整问答正文)
这两类文本是很好的教学素材:它们格式规整、来源权威、内容金融属性强,而且问答类文本篇幅适中,方便学生阅读原文后与分析结果对比。
25.1.2 文本分析的第一步:定义观测单位
开始写代码之前,先要回答一个问题:每一行代表什么?
这听起来像废话,但它实际上决定了后续所有分析的逻辑。如果你的数据里一行是一篇完整年报,和一行是年报里的一个段落,做出来的词频统计含义完全不同。
对本章的数据而言:
pbc_news_raw.csv:每一行是一条新闻(一个观测单位)pbc_qa_raw.csv:每一行是一篇答记者问(一个观测单位)
25.1.3 最终都要整理成一张表
无论原始数据从哪里来——网页、PDF、API 还是手工整理——进入分析之前,都应该先整理成规则表格。一个标准的文本数据表结构如下:
| 字段 | 含义 | 示例 |
|---|---|---|
title |
标题 | 中国人民银行召开 2026 年金融稳定工作会议 |
date |
日期 | 2026/3/27 |
url |
原文链接 | https://www.pbc.gov.cn/… |
text |
正文 | 会议听取了… |
section |
栏目/类别 | pbc_news |
crawl_time |
抓取时间 | 2026/3/29 |
原则:先整理成表格,再做分析。
很多初学者习惯把原始文本直接扔给模型,让模型直接给结论。这种方式在演示层面看起来省事,但结果难以复现,也无法大规模处理。更稳妥的顺序是:定义观测单位 → 整理为表格 → 检查与清洗 → 分词与分析。
25.2 读取数据与初步检查
25.2.1 为什么读进来之后不能立刻分析
「文件能读进来」不等于「数据已经可用」。文本数据里常见的问题包括:
- 正文列抓到的是导航菜单而不是真正的内容
- 同一条新闻被抓了两次
- 日期格式不统一,无法做时间序列分析
- 部分记录的正文极短,可能是抓取失败
这些问题如果不在早期发现,等到分词之后再排查会麻烦很多。建议养成习惯:读入数据后,先花 5 分钟做初步检查,再进入分析。
25.2.2 读取数据
本章数据文件使用 GBK 编码(Windows 中文系统的默认编码)。如果你在 macOS 或 Linux 上运行, 遇到 UnicodeDecodeError,可以尝试以下备选方案:
# 方案 1:改用 utf-8-sig(带 BOM 的 UTF-8)
df = pd.read_csv("data_raw/pbc_news_raw.csv", encoding="utf-8-sig")
# 方案 2:让 pandas 自动检测编码(需要安装 chardet)
import chardet
with open("data_raw/pbc_news_raw.csv", "rb") as f:
enc = chardet.detect(f.read(10000))["encoding"]
df = pd.read_csv("data_raw/pbc_news_raw.csv", encoding=enc)先看前几行,对数据内容建立基本印象。
# 查看前 3 行,重点看 title、date、text 三列
df[["title", "date", "text", "section"]].head(3)25.2.3 检查字段类型与缺失值
# 查看各字段的数据类型和非空数量
df.info()# 重点检查关键字段的缺失情况
missing = df[["title", "date", "text"]].isna().sum()
print("关键字段缺失值统计:")
print(missing)25.2.4 检查文本长度
文本长度是排查异常的好抓手。正文过短往往意味着抓取失败,或者抓到的是导航文字而不是真正的内容。
import matplotlib.pyplot as plt
# 计算正文字符长度
df["text_len"] = df["text"].fillna("").str.len()
print("正文长度描述统计:")
print(df["text_len"].describe().round(0))
# 绘制长度分布直方图,快速发现异常
plt.rcParams["font.family"] = "SimHei" # Windows 用 SimHei
# plt.rcParams["font.family"] = "PingFang SC" # macOS 用户可改这行
plt.rcParams["axes.unicode_minus"] = False
plt.figure(figsize=(10, 4))
plt.hist(df["text_len"], bins=30, edgecolor="white")
plt.xlabel("正文字符数")
plt.ylabel("文章数量")
plt.title("人民银行新闻正文长度分布")
plt.savefig("Fig/text_len_dist.png", dpi=150, bbox_inches="tight")
plt.show()# 查看正文最短的 5 条,判断是否是抓取异常
df.sort_values("text_len")[["title", "date", "text_len", "text"]].head(5)25.2.5 检查重复值和日期格式
# 检查 URL 重复(URL 重复通常意味着同一条新闻被抓了多次)
dup_count = df["url"].duplicated().sum()
print(f"重复 URL 数量:{dup_count}")
# 将日期转为标准格式,顺便找出格式异常的记录
df["date_std"] = pd.to_datetime(df["date"], errors="coerce")
bad_dates = df[df["date_std"].isna()][["date", "title"]]
print(f"日期解析失败数量:{len(bad_dates)}")
if len(bad_dates) > 0:
print(bad_dates)检查完毕,对数据的基本质量有了判断之后,再进入清洗环节。
我有一个中文文本 DataFrame,字段包括 title、date、url、text、section。请用 Python 编写一个可复用的检查函数 audit_text_df(df),要求输出以下信息:
- 行数、列数、字段名与数据类型
title、date、text三列的缺失值统计text列的字符长度分布(min、均值、中位数、max)- 正文最短的 5 条记录
- URL 重复数量
date字段能否成功转为标准日期格式- 最后给出一句「是否建议直接进入下一步」的结论
要求:所有代码加详细中文注释,函数写法清晰,适合日后复用到其他文本数据集。
25.3 文本清洗:去噪、标准化与去重
25.3.1 清洗的目标是什么
原始文本里常见的「脏东西」大致有这几类:
- 多余空格和换行:爬虫抓取时保留了原网页的排版符号
- URL 地址:正文里夹杂了超链接地址
- 噪声短语:「责任编辑」「打印本页」「来源:新华社」等页脚/页眉信息
- 重复记录:同一条新闻被抓了两次
如果不先清洗,这些噪声会直接影响后面的分词和词频统计——「打印」「责任」这类词会莫名其妙地出现在高频词里。
有一点要强调:不要直接覆盖原始文本列,而是新建一列 text_clean。这样原始数据还在,清洗逻辑可以随时修改。
25.3.2 一个基础清洗函数
import re
import pandas as pd
def clean_text(text):
"""对单条中文文本做基础清洗,返回清洗后的字符串。"""
if not isinstance(text, str):
return ""
# 去除 URL(http:// 或 www. 开头的链接)
text = re.sub(r"http[s]?://\S+|www\.\S+", "", text)
# 去除 HTML 标签(如 <p>、<br/> 等)
text = re.sub(r"<[^>]+>", "", text)
# 去除常见页脚噪声词
noise_patterns = ["责任编辑", "打印本页", "关闭窗口", "来源:", "编辑:"]
for pattern in noise_patterns:
text = text.replace(pattern, "")
# 把换行符和制表符替换为空格,然后合并连续空格
text = text.replace("\n", " ").replace("\t", " ")
text = re.sub(r"\s+", " ", text)
return text.strip()
# 生成清洗后的新列,保留原始 text 列不动
df["text_clean"] = df["text"].apply(clean_text)
print("清洗完成,新增 text_clean 列")
print(f"清洗前平均长度:{df['text'].str.len().mean():.0f} 字符")
print(f"清洗后平均长度:{df['text_clean'].str.len().mean():.0f} 字符")清洗完之后,随机抽几条对比一下,确认没有误删有用内容。
# 随机抽取 3 条,对比清洗前后
sample = df.sample(3, random_state=42)[["title", "text", "text_clean"]]
for _, row in sample.iterrows():
print(f"【标题】{row['title']}")
print(f"【清洗前】{str(row['text'])[:100]}...")
print(f"【清洗后】{str(row['text_clean'])[:100]}...")
print("-" * 60)25.3.3 去重
# 优先用 URL 去重(最准确),URL 缺失时用 title+date 作为备选
df_clean = df.drop_duplicates(subset=["url"]).copy()
# 把检查阶段生成的标准化日期列一并带过来,后续时序分析会用到
df_clean["date_std"] = df_clean["date"].pipe(pd.to_datetime, errors="coerce")
print(f"去重前:{len(df)} 条,去重后:{len(df_clean)} 条")清洗不是越多越好。
有些初学者会把正则表达式写得很激进,把数字、标点、括号全部去掉。对于词频统计来说,这样做可能反而有损信息——比如,「降准 0.5 个百分点」中的数字是有意义的,不应该被去掉。
清洗后一定要抽样对比,确认有用内容没有被误删。
我有一个 DataFrame,其中 text 列包含从人民银行官网爬取的中文新闻正文,可能混有 URL、换行符、页脚信息(如「责任编辑」「打印本页」)。请你用 Python 编写清洗函数,要求:
- 去除 URL、HTML 标签、换行符
- 去除「责任编辑」「打印本页」「来源:」等常见页脚噪声
- 合并连续空格
- 保留原始
text列,新建text_clean列 - 随机抽取 5 条,打印清洗前后对比
- 所有代码加中文注释
25.4 中文分词、停用词与自定义词典
25.4.1 为什么中文需要分词
英文文本天然由空格分隔,可以直接统计单词频率。中文没有空格,「中国人民银行宣布降准」这一句话,如果不做处理,计算机只能一个字一个字地读,无法识别「人民银行」「降准」是完整的词。
分词的作用,就是把连续的汉字切成有意义的词序列:
原文:中国人民银行宣布降准
分词:中国 / 人民银行 / 宣布 / 降准
分词结果的质量直接决定后续分析的质量。如果「人民银行」被切成「人民 / 银行」,或者「逆回购」被切成「逆 / 回购」,那么后续的词频统计和关键词提取就都建立在错误的基础上了。
25.4.2 停用词:要过滤掉的高频废词
分词之后,通常会出现大量高频但没有分析价值的词,比如「的」「了」「是」「在」,以及金融文本中常见的套话:「相关」「工作」「进一步」「有关」。这类词叫做停用词,需要用停用词表过滤掉。
停用词表不是一次性配置好就完事的。每完成一轮分词,看看高频词里有没有新的废词,就往停用词表里补一补——这是个小型迭代过程。
25.4.3 自定义词典:金融专业术语的保障
jieba 的默认词典是通用词典,对金融专业术语的支持有限。如果不加自定义词典:
- 「中期借贷便利」→ 被切成「中期 / 借贷 / 便利」
- 「结构性货币政策工具」→ 被切成「结构性 / 货币政策 / 工具」
- 「宏观审慎管理」→ 被切成「宏观 / 审慎 / 管理」
词典文件格式很简单,每行一个词(可以加词频和词性,也可以只写词):
中期借贷便利 5 n
结构性货币政策工具 5 n
宏观审慎管理 5 n
25.4.4 完整分词流程
25.4.5 jieba 的三种分词模式
jieba 提供三种分词模式,理解它们的区别有助于选择合适的模式:
| 模式 | 特点 | 适合场景 |
|---|---|---|
精确模式(lcut) |
切分精准,不重叠 | 词频统计、TF-IDF(本章用这个) |
全模式(lcut(..., cut_all=True)) |
穷举所有可能的词,有重叠 | 快速浏览语料,不用于统计 |
搜索引擎模式(lcut_for_search) |
在精确模式基础上对长词再细切 | 搜索索引构建 |
用同一句话对比三种模式的输出:
import jieba
sent = "中国人民银行开展中期借贷便利操作,维护银行体系流动性合理充裕"
print("【精确模式】(词频统计的标准选择)")
print(" / ".join(jieba.lcut(sent)))
print("\n【全模式】(穷举所有可能词,有重叠,不适合统计)")
print(" / ".join(jieba.lcut(sent, cut_all=True)))
print("\n【搜索引擎模式】(长词会被进一步细切)")
print(" / ".join(jieba.lcut_for_search(sent)))# !pip install jieba # 第一次使用时取消注释安装
import jieba
import os
# ---------- 词典路径(按你的实际目录调整)----------
DICT_DIR = "dicts"
STOPWORDS_FILE = os.path.join(DICT_DIR, "stopwords_cn_finance.txt")
USER_DICT_FILE = os.path.join(DICT_DIR, "user_dict_finance.txt")
# 加载自定义金融词典(优先识别词典里的专业术语,避免被切错)
if os.path.exists(USER_DICT_FILE):
jieba.load_userdict(USER_DICT_FILE)
print(f"已加载用户词典:{USER_DICT_FILE}")
else:
print("未找到用户词典,继续使用默认词典")
# 读取停用词表
with open(STOPWORDS_FILE, "r", encoding="utf-8") as f:
stopwords = set(line.strip() for line in f if line.strip())
print(f"停用词表加载完成,共 {len(stopwords)} 个停用词")
print(f"示例停用词:{list(stopwords)[:8]}")def tokenize(text):
"""对单条中文文本分词,并过滤停用词和过短词项。"""
if not isinstance(text, str) or not text.strip():
return []
words = jieba.lcut(text) # 精确模式分词
# 过滤停用词、长度小于 2 的词、纯数字
words = [
w for w in words
if w.strip() # 排除空字符
and w not in stopwords # 排除停用词
and len(w) >= 2 # 排除单字(信息量通常较低)
and not w.strip().isdigit() # 排除纯数字
]
return words
# 对清洗后的正文做分词,结果保存为列表
df_clean["tokens"] = df_clean["text_clean"].apply(tokenize)
print("分词完成,查看前 2 条结果:")
for _, row in df_clean.head(2).iterrows():
print(f"标题:{row['title']}")
print(f"分词:{row['tokens'][:15]}...")
print()25.4.6 验证自定义词典是否生效
加载词典之后,用几个典型术语验证一下效果,这一步不能省。
# 用几个典型金融术语测试分词效果
test_sentences = [
"中国人民银行开展中期借贷便利操作",
"加强宏观审慎管理,防范系统性金融风险",
"结构性货币政策工具精准滴灌实体经济",
]
for sent in test_sentences:
tokens = jieba.lcut(sent)
print(f"原文:{sent}")
print(f"分词:{' / '.join(tokens)}")
print()分词结果不好时,应该怎么排查?
不要只在分词这一步反复调参,而应顺着流程向前回溯:
- 关键术语被拆开 → 补充自定义词典,或用
jieba.add_word()临时添加 - 虚词仍大量出现 → 扩充停用词表
- URL 或页脚噪声残留 → 回到清洗环节增加规则
- 单字词过多 → 适当提高
len(w) >= 2的过滤门槛
分词是一个小型迭代过程:做完第一轮,抽样看结果,发现问题,修改词典或停用词表,再做第二轮。
我已经对中文金融文本完成了分词,结果保存在 tokens 列(列表格式)。请帮我诊断分词质量:
- 数据背景:人民银行官网新闻,字段包括
title、text_clean、tokens - 问题描述:我怀疑分词结果不够准确
- 请完成:① 随机抽取 10 条显示分词结果;② 统计全样本 Top 30 高频词;③ 判断是否存在术语被拆开、虚词过多、噪声残留等问题;④ 给出「问题 → 修正建议」清单
- 要求:所有代码加中文注释,最后总结应优先修改哪些内容
25.5 高频词统计与可视化
25.5.1 为什么先看高频词
完成分词之后,最先能做的就是统计词频。这一步的主要用途有两个:一是对语料的整体内容建立直觉;二是诊断前面的清洗和分词是否到位——如果高频词里还有很多废词,就说明停用词表或分词规则还需要调整。
25.5.2 全样本高频词统计
from collections import Counter
import pandas as pd
import matplotlib.pyplot as plt
plt.rcParams["font.family"] = "SimHei"
# plt.rcParams["font.family"] = "PingFang SC" # macOS 用户改这行
plt.rcParams["axes.unicode_minus"] = False
# 合并所有文章的词项,统计全样本词频
all_words = []
for tokens in df_clean["tokens"]:
all_words.extend(tokens)
word_freq = Counter(all_words)
top20 = word_freq.most_common(20)
print("全样本 Top 20 高频词:")
for word, freq in top20:
print(f" {word}:{freq} 次")25.5.3 词频柱状图
top20_df = pd.DataFrame(top20, columns=["词语", "频次"])
plt.figure(figsize=(10, 6))
# 倒序排列,让频次最高的词显示在顶部
plt.barh(top20_df["词语"][::-1], top20_df["频次"][::-1], color="steelblue")
plt.xlabel("出现次数")
plt.title("人民银行新闻 Top 20 高频词(2025.11–2026.03)")
plt.tight_layout()
plt.savefig("Fig/top20_words_bar.png", dpi=150, bbox_inches="tight")
plt.show()25.5.4 词云图
# !pip install wordcloud # 第一次使用时取消注释
from wordcloud import WordCloud
import matplotlib.pyplot as plt
# 中文词云必须指定字体路径,否则会显示方块
font_path = "C:/Windows/Fonts/simhei.ttf" # Windows
# font_path = "/System/Library/Fonts/PingFang.ttc" # macOS
# 将词频字典传入 WordCloud,generate_from_frequencies 直接用统计好的词频
wc = WordCloud(
font_path=font_path,
width=800,
height=400,
background_color="white",
max_words=80,
colormap="Blues",
)
wc.generate_from_frequencies(word_freq)
plt.figure(figsize=(12, 5))
plt.imshow(wc, interpolation="bilinear")
plt.axis("off")
plt.title("人民银行新闻词云(2025.11–2026.03)")
plt.savefig("Fig/wordcloud_pbc_news.png", dpi=150, bbox_inches="tight")
plt.show()词云在展示上直观好看,但在正式分析中,柱状图更有用——它能精确显示频次,方便比较。两者配合使用是常见做法。
25.5.5 按时间段对比词频变化
如果数据跨越了多个时间段,比较不同时期的高频词往往比看全样本更有价值。人民银行的政策语言会随经济形势演变,这种对比能直观呈现这种变化。
import pandas as pd
from collections import Counter
import matplotlib.pyplot as plt
# 按季度划分,对比不同时期的高频词
df_clean["quarter"] = df_clean["date_std"].dt.to_period("Q")
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
# 取数据中最早和最晚两个季度进行对比
quarters = sorted(df_clean["quarter"].dropna().unique())
selected = [quarters[0], quarters[-1]] # 最早和最晚季度
for ax, q in zip(axes, selected):
subset = df_clean[df_clean["quarter"] == q]
words = []
for tokens in subset["tokens"]:
words.extend(tokens)
top10 = pd.DataFrame(Counter(words).most_common(10), columns=["词语", "频次"])
ax.barh(top10["词语"][::-1], top10["频次"][::-1], color="steelblue")
ax.set_title(f"{q} Top 10 高频词")
ax.set_xlabel("频次")
plt.suptitle("人民银行新闻不同时期高频词对比", fontsize=13)
plt.tight_layout()
plt.savefig("Fig/top10_by_quarter.png", dpi=150, bbox_inches="tight")
plt.show()我有一个 DataFrame,tokens 列保存了每条中文文本的分词列表,date_std 列是标准日期格式。请完成:
- 合并所有词项,统计全样本 Top 20 高频词,绘制横向柱状图
- 用
matplotlib生成词云图(需指定中文字体路径) - 按季度分组,对比最早和最晚季度各自的 Top 10 高频词,并排展示
- 用 2-3 句话解读对比结果,说明哪些词的频次变化值得关注
要求:所有代码加中文注释,图表标题和坐标轴标签用中文,图表保存到 Fig/ 文件夹。
25.6 TF-IDF:找出真正有区分度的词
25.6.1 高频词的局限
去掉停用词之后,「金融」「发展」「稳定」「推进」这类词往往还是高频词。它们确实出现得多,但问题是:它们在每一篇文章里都出现得多,所以它们对区分「这篇文章在讲什么」没有太大帮助。
换句话说,高频词回答的是「整个语料里什么词最常见」,而我们有时候更想知道的是「哪些词能最好地代表这篇文章」。这正是 TF-IDF 要解决的问题。
25.6.2 TF-IDF 的直觉
TF-IDF 的逻辑很简单:
- TF(词频):这个词在这篇文章里出现了多少次?
- IDF(逆文档频率):这个词在所有文章里有多常见?越常见,权重越低
- TF-IDF = TF × IDF:只有「在本文高频、在全局不那么普遍」的词,才会得到高权重
\[ \text{TF-IDF}_{d,w} = tf_{d,w} \times \log\left(\frac{N}{df_w}\right) \]
其中:\(tf_{d,w}\) 是词 \(w\) 在文档 \(d\) 中的词频,\(N\) 是文档总数,\(df_w\) 是包含词 \(w\) 的文档数。
一个金融类比:假设你在分析 2025 年第四季度的人民银行新闻,发现「跨境支付」在某篇文章里反复出现,但在其他文章里很少提到——这篇文章的 TF-IDF 就会给「跨境支付」很高的权重,说明这是这篇文章的核心话题。而「金融」「发展」这类词,虽然词频高,但因为在所有文章里都高频,IDF 值很低,最终 TF-IDF 权重也低。
TF-IDF 的效果高度依赖前面的分词和停用词处理。 如果分词结果里有大量错误,或者停用词没有过滤干净,TF-IDF 提取出来的「关键词」也不会有什么解释力。这是一条流水线,每一步都影响下一步。
25.6.3 用 scikit-learn 计算 TF-IDF
from sklearn.feature_extraction.text import TfidfVectorizer
import pandas as pd
# TfidfVectorizer 输入的是字符串,所以先把 tokens 列表拼成空格分隔的字符串
# (jieba 分词后以空格连接,让 TfidfVectorizer 按空格切词)
df_clean["text_for_tfidf"] = df_clean["tokens"].apply(lambda x: " ".join(x))
# 构建 TF-IDF 矩阵
# analyzer='word':按词计算(我们已经完成了中文分词)
# token_pattern=r'(?u)\b\w+\b':匹配任意长度的词,避免单字母被过滤
vectorizer = TfidfVectorizer(analyzer="word", token_pattern=r"(?u)\b\w+\b")
X_tfidf = vectorizer.fit_transform(df_clean["text_for_tfidf"])
print(f"TF-IDF 矩阵维度:{X_tfidf.shape}")
print(f"即:{X_tfidf.shape[0]} 篇文章 × {X_tfidf.shape[1]} 个词项")25.6.4 提取单篇文章的关键词
import numpy as np
feature_names = vectorizer.get_feature_names_out()
def get_top_keywords(doc_index, top_n=10):
"""提取指定文章的 TF-IDF 关键词。"""
row = X_tfidf[doc_index].toarray().flatten()
top_idx = np.argsort(row)[::-1][:top_n]
keywords = [(feature_names[i], round(row[i], 4)) for i in top_idx if row[i] > 0]
return keywords
# 展示前 3 篇文章的关键词
for i in range(3):
title = df_clean.iloc[i]["title"]
keywords = get_top_keywords(i)
print(f"【文章 {i+1}】{title}")
print(f" TF-IDF 关键词:{[kw for kw, _ in keywords]}")
print()25.6.5 对比不同类型文章的 TF-IDF 关键词
TF-IDF 的更大价值在于多文档对比。我们把人民银行新闻(pbc_news)和答记者问(pbc_qa)合并,看看两类文本的关键词有什么差异。
import pandas as pd
from sklearn.feature_extraction.text import TfidfVectorizer
import numpy as np
# 读取答记者问数据(编码同样是 GBK)
# 答记者问数据同样是 GBK 编码
df_qa = pd.read_csv("data_raw/pbc_qa_raw.csv", encoding="gbk")
df_qa["text_clean"] = df_qa["text"].apply(clean_text)
df_qa["tokens"] = df_qa["text_clean"].apply(tokenize)
df_qa["text_for_tfidf"] = df_qa["tokens"].apply(lambda x: " ".join(x))
# 把两类文本拼在一起
df_news_sub = df_clean[["title", "section", "text_for_tfidf"]].copy()
df_qa_sub = df_qa[["title", "section", "text_for_tfidf"]].copy()
df_all = pd.concat([df_news_sub, df_qa_sub], ignore_index=True)
# 确认两类文本都有记录,避免 section 列有 NaN 导致静默丢数据
print(df_all["section"].value_counts())
assert df_all["section"].isna().sum() == 0, "section 列存在缺失值,请检查数据"
# 对合并后的数据重新计算 TF-IDF
vec2 = TfidfVectorizer(analyzer="word", token_pattern=r"(?u)\b\w+\b")
X2 = vec2.fit_transform(df_all["text_for_tfidf"])
feat2 = vec2.get_feature_names_out()
# 分别计算两类文本的平均 TF-IDF,找出各自最有代表性的词
news_mask = df_all["section"] == "pbc_news"
qa_mask = df_all["section"] == "pbc_qa"
news_mean = X2[news_mask.values].mean(axis=0).A1
qa_mean = X2[qa_mask.values].mean(axis=0).A1
top_news = [(feat2[i], round(news_mean[i], 4)) for i in np.argsort(news_mean)[::-1][:10]]
top_qa = [(feat2[i], round(qa_mean[i], 4)) for i in np.argsort(qa_mean)[::-1][:10]]
print("【pbc_news】最具代表性的词:")
print([w for w, _ in top_news])
print()
print("【pbc_qa】最具代表性的词:")
print([w for w, _ in top_qa])这个结果很直观:新闻类文本的高权重词往往是会议名称、人物职位、活动类型;而答记者问的高权重词更多是政策术语和分析性词汇。这正是 TF-IDF 的价值所在——它帮你找到两类文本「各自更像自己」的词。
TF-IDF 同样适用于纵向对比。 除了横向对比不同类型文本,TF-IDF 也很适合对比同一来源在不同时间段的用词变化——比如,人民银行 2023 年和 2025 年新闻的关键词差异,往往能直接反映货币政策基调的转变。这比只看高频词更灵敏。
我有一个 DataFrame,tokens 列是每条文本的分词列表,section 列标注了文本类别(pbc_news 和 pbc_qa)。请完成:
- 把
tokens拼成空格分隔的字符串,用TfidfVectorizer构建 TF-IDF 矩阵 - 分别计算两类文本的平均 TF-IDF 权重,提取各自 Top 10 关键词
- 用并排柱状图展示两类文本的关键词差异
- 用 2-3 句话解读:两类文本的用词侧重有什么不同?
要求:所有代码加中文注释,图表保存到 Fig/ 文件夹。
25.7 本章小结与练习
25.7.1 本章技能清单
| 任务 | 工具 | 关键注意点 |
|---|---|---|
| 读取文本数据 | pandas |
注意编码(GBK / UTF-8) |
| 初步检查 | pandas |
看长度分布、缺失值、重复 URL |
| 文本清洗 | re, pandas |
保留原列,新建 text_clean |
| 中文分词 | jieba |
必须加载自定义词典 |
| 停用词过滤 | 自定义词表 | 迭代更新,不是一次性配置 |
| 词频统计 | collections.Counter |
同时用柱状图和词云 |
| 关键词提取 | scikit-learn TfidfVectorizer |
先用 jieba 分词,再传入 |
25.7.2 本章的工作流
\[ \text{原始文本} \xrightarrow{\text{读取+检查}} \text{text\_clean} \xrightarrow{\text{分词+停用词}} \text{tokens} \xrightarrow{\text{词频/TF-IDF}} \text{可分析指标} \]
有两点值得反复强调:
第一,这是一个带反馈的迭代过程,不是单向的流水线。如果 TF-IDF 的关键词不对,可能是分词的问题;如果分词不对,可能是清洗不够彻底;如果清洗有问题,可能是观测单位定义就不清楚。发现问题时,顺着流程往前回溯,而不是只在当前步骤打补丁。
第二,文本分析中很多问题不是「模型问题」,而是「数据结构和预处理问题」。在真实工作中,70% 的时间往往花在读取、检查和清洗上,而不是在建模上。
25.7.3 下一章预告
本章我们完成了「文本变成数据」这件事。下一章将在此基础上继续:如何用词典法和大语言模型对文本做情感分析,如何用情感指数追踪政策语气的变化,以及主题模型的基本思路。
另外,本章使用的 120 条新闻和 185 条问答,是由爬虫脚本自动采集的。如果你想自己去采集更多文本——比如某一行业所有上市公司的年报、或者某个时间段内的所有政策文件——下下章「网络爬虫」会教你怎么做,那也是本章这套分析方法真正发挥规模效应的前提。
25.7.4 课后练习
使用
pbc_qa_raw.csv,完成完整的「读取 → 检查 → 清洗 → 分词 → 词频统计」流程,观察答记者问数据与新闻数据的高频词有什么不同。修改停用词表,加入你认为在人民银行文本中没有分析价值的词(比如「我国」「有关」「方面」),重新统计高频词,对比修改前后的 Top 20 变化。
随机抽取 10 条文本,仔细阅读原文,然后查看它们的 TF-IDF 关键词——你觉得关键词能准确反映文章的核心内容吗?如果不能,可能是哪个环节的问题?
把
pbc_news_raw.csv和pbc_qa_raw.csv合并,构建 TF-IDF 矩阵,对比两类文本各自 Top 10 关键词的差异,并用 2-3 句话解读这种差异背后的含义。进阶练习:在
finance_dict.txt里补充至少 10 个你认为重要的金融术语,重新分词,对比前后关键词的变化。
25.8 参考文献
- Loughran, T., & McDonald, B. (2011). When is a liability not a liability? Textual analysis, dictionaries, and 10-Ks. The Journal of Finance, 66(1), 35–65.
- 结巴中文分词:https://github.com/fxsjy/jieba
- 清华大学开放中文词库(THUOCL):https://github.com/thunlp/THUOCL