news 2026/9/24 1:04:26

微博热点舆情聚类实战:从爬虫清洗到TF-IDF与KMeans的完整链路

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
微博热点舆情聚类实战:从爬虫清洗到TF-IDF与KMeans的完整链路

简介:面向对Python文本挖掘与舆情分析感兴趣的学习者,资源以微博热点话题为对象,完整提供了从数据采集、分词处理到聚类分析的项目源码与配套数据。核心依赖包括jieba分词、pandas数据处理、scikit-learn机器学习、matplotlib可视化与requests网络请求,适合用于入门自然语言处理、掌握中文分词与文档聚类流程。压缩包仅50KB,共9个文件,包含5个Python脚本、2个文本数据文件、1个说明文档和1个CSV数据集,脚本分工清晰,涵盖爬虫采集、TF-IDF计算、矩阵构建与可视化等环节,便于对照学习。目前已有209人浏览学习,结合源码与数据可直观理解舆情热点聚类的完整实现路径,从数据抓取、分词清洗到结果可视化环环相扣。项目整体虽轻量,但流程完整,特别适合快速掌握聚类分析每一步的计算逻辑,也可作为课程设计或研究入门参考,为后续扩展情感分析、主题模型等方向留下清晰改造空间。

1. 用微博热点做舆情聚类,难的不是爬数据,而是让文本变成能算的东西

舆情聚类分析这个方向,很多人的第一反应是“先去爬微博”,结果卡在登录、封 IP、验证码上三天没进展。真正做过一轮之后你会发现,微博热点话题的采集反而是整个流程里最不费脑子的部分——数据拿到手之后,分词、去停用词、向量化、调聚类参数,每一步都在消耗你的耐心。尤其是热点话题这种短文本,本身长度短、口语化严重、网络新词多,直接套用标准 NLP 流程很容易跑出一个“看似收敛、实际全挤在一个簇里”的结果。

这个项目标题里“源码+数据”的组合,对应的是一条完整可复现的链路:拿到微博热点话题数据,做中文文本清洗,转成 TF-IDF 向量,再用聚类算法分簇,最后输出每个簇的代表词和热度分布。它适合谁?一类是刚开始接触文本挖掘的学生或转行开发者,想找一个不是豆瓣影评、不是新闻稿的实战项目;另一类是做舆情分析或社交媒体运营的从业者,需要从一堆话题里快速看出哪些事件在抱团发酵。前者能学到完整流程,后者能直接拿到一套可以改数据源就跑的分析脚本。

2. 微博热点数据从哪来:三种采集方案的选型与拆解

2.1 先判断手里有没有现成数据,再决定要不要写爬虫

标题里“源码+数据”意味着这份项目大概率自带一份已经抓好的微博数据。拿到项目之后第一件事不是读代码,而是看数据文件的格式和字段。常见的格式有两种:CSV 和 JSON。CSV 一般长这样:

id,created_at,text,reposts_count,comments_count,attitudes_count,hot_word 1001,2024-11-20 14:23:01,某明星新电影发布首支预告,1234,892,5678,电影预告 1002,2024-11-20 14:25:44,这部电影的造型设计太惊艳了,456,233,1200,电影造型

JSON 则往往是微博 API 或爬虫抓取时未处理的原始返回结构,嵌套了一层“user”“retweeted_status”之类的对象。我一般会先写一段极简的预览脚本,确认字段名和缺失情况再决定后续清洗策略:

import pandas as pd df = pd.read_csv('weibo_hot.csv', encoding='utf-8') print(df.shape) print(df.columns.tolist()) print(df.head(3).to_string()) # 检查缺失值占比,超过 20% 的字段后面大概率用不上 print(df.isnull().mean().sort_values(ascending=False))

这段代码本身没什么技术含量,但它的价值在于让你在动手前心里有底——数据是干净规整的还是带了一堆爬虫异常记录。注意encoding参数,微博数据的 CSV 用utf-8读取报错时,改成gbkutf-8-sig再试,这是最常见的一个坑。

如果你手里没有这份数据,只想先跑通流程,常见做法是自己构造一份小样本数据来验证聚类效果,把后面要讲的流程跑通后再去采集真实数据。用pandas构造几百条微博文本做联调,成本远低于一上来就攻坚采集。

2.2 官方 API 和爬虫的取舍:没有绝对方案,只有当前条件下的最优解

微博开放平台的历史 API 几乎全部收紧了权限,个人开发者能拿到的基础接口非常有限,数据时效性和完整性也打了折扣。项目如果标注“源码+数据”,大概率用的是基于网页版的爬虫方案。业界常见的实现思路是:模拟登录拿到 Cookie,带 Cookie 请求热点话题列表页或搜索页,解析返回的 HTML 或 JSON 数据。

一个相对温和、可以跑通全流程的代码框架如下:

import requests import json import time # 说明:此处仅演示请求与解析结构,实际使用需遵守目标平台服务条款 headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", "Cookie": "你的登录凭证" # 从浏览器开发者工具中复制,勿硬编码在正式代码里 } def fetch_hot_topic(page=1): """拉取某一页的热门话题数据,返回解析后的结构化记录""" url = "https://weibo.com/ajax/statuses/hot_band" params = {"page": page} resp = requests.get(url, headers=headers, params=params, timeout=10) # 正常返回是 JSON 结构,直接解析即可 data = resp.json() records = [] for item in data.get("data", {}).get("band_list", []): if not item.get("note"): continue # 跳过空话题 records.append({ "hot_word": item.get("note"), "num": item.get("num"), # 热度值 "category": item.get("category"), "raw_text": item.get("raw_text") or item.get("note") }) return records if __name__ == "__main__": all_records = [] for p in range(1, 3): # 先抓两页验证 all_records.extend(fetch_hot_topic(p)) time.sleep(2) # 控制请求频率,别把自己玩进风控名单 print(len(all_records))

这段代码里值得解释的参数有四个。timeout=10是请求超时时间,微博接口偶尔会慢,设短了容易误判失败,设长了请求会堆积。page是分页参数,热点话题的返回顺序是按热度降序排列的。time.sleep(2)是请求间隔,写死 2 秒是一个兼顾效率和风控的折中值。headers里的Cookie会有有效期,爬的数量大或运行时间长要设计成从配置读取、过期后手动更新。

关于采集,需要提醒一句:不同平台的接口结构会随时调整,代码里解析的字段名可能某天就失效了。如果你的目标是快速跑通舆情聚类分析流程,建议优先用项目自带数据;爬虫只是补充手段,不是主菜。

2.3 多关键词增量采集:让数据不止于“热点榜”这一个入口

热点榜数据有个问题:它在某个时间点只能反映当前最热的事件,而对一个话题的“生命周期”刻画不足。做舆情聚类分析时,我一般还会按热点词去搜相关微博,把单条话题的内容铺开。这里只需要把请求地址替换成搜索接口,同时增加一个关键词参数即可。

def search_weibo(keyword: str, page: int = 1): """按关键词搜索微博,返回文本列表""" url = "https://weibo.com/ajax/statuses/search" params = { "q": keyword, "page": page, "sort": "time" # 按时间排序,舆情分析一般关注最新进展 } resp = requests.get(url, headers=headers, params=params, timeout=10) data = resp.json() texts = [item.get("text_raw", "") for item in data.get("data", {}).get("list", [])] return texts # 用法示例:把热搜榜上的前 5 个词各拉 3 页数据 # hot_words = [r["hot_word"] for r in all_records[:5]] # for w in hot_words: # for pg in range(1, 4): # texts = search_weibo(w, pg) # # 存入本地文件或数据库

这里的sort参数是舆情分析里的关键选择——按热度排序会丢过程、按时间排序才能看到话题的发酵路径。我一般会同时抓两种排序结果,分析时分开看。text_raw是微博的原始文本字段,注意这个字段里可能包含 HTML 标签和转义字符,后面清洗时要处理。

3. 从微博文本到聚类输入:分词、去停用词和 TF-IDF 向量化的完整链路

3.1 清洗规则怎么定:去掉的内容和保留的内容同等重要

微博短文本的清洗有它的特殊性。新闻语料清洗主要去掉空白和标点,微博文本则要处理 @用户、话题标签、URL、表情符号和“转发微博”这类冗余文字。一个小技巧:不要用正则去匹配所有特殊字符,而是先想清楚你要保留什么。中文分析中,汉字、数字和部分中英文标点是有意义的;@、#、URL 是没意义的。下面是我常用的清洗函数:

import re def clean_weibo_text(text: str) -> str: """清洗微博文本:去 URL、@用户、话题符号、多余空白""" # 去掉 http(s) 链接 text = re.sub(r'https?://\S+', '', text) # 去掉 @用户 text = re.sub(r'@[\w\u4e00-\u9fa5\-]+', '', text) # 去掉 #话题# 两端的井号但仍保留内部文字 text = re.sub(r'#([^#]+)#', r'\1', text) # 去掉 HTML 标签和实体 text = re.sub(r'<[^>]+>', '', text) text = re.sub(r'&[a-zA-Z]+;', '', text) # 合并多个空格 text = re.sub(r'\s+', ' ', text).strip() return text # 示例 sample = "转发微博 #电影首发# 这部电影太棒了!@电影官博 https://t.cn/abc123" print(clean_weibo_text(sample)) # 输出: 转发微博 电影首发 这部电影太棒了!

清洗函数的顺序是有讲究的。先去掉 URL 再处理 @,是因为链接中可能包含 @ 字符,先处理链接可以避免误伤。#话题#的处理不是直接删掉而是保留文字,因为话题本身携带了关键主题信息,对聚类是有用的。HTML 标签处理放在后面,是因为某些接口返回的text_raw字段里会有<a>之类的标签包裹超链接,先解 URL 再解标签顺序更稳定。注意,严格说text_raw通常是纯文本字段,HTML 标签多见于text字段,两个字段采集时都可以拿到,清洗时视情况保留哪个需要自己确认。

3.2 jieba 分词与用户词典:热点词是舆情聚类的生命线

微博舆情聚类的分词不能直接裸用 jieba 默认模型。原因在于热点话题里充满了人名、作品名、品牌名和网络新词,比如某明星的缩写、某综艺的简称,默认词典压根没有。业界常规做法是准备一个自定义词典,把从热点榜上收集到的话题词、相关实体名放进去,再配合 jieba 加载:

import jieba # 把热点词和相关词写入词典文件 custom_words = ["某明星", "电影首发", "数据要素", "低空经济"] for w in custom_words: jieba.add_word(w, freq=10000) # freq 是词频权重,越大越容易被切成一个词 text = "某明星新电影首发预告引发全网热议" seg_list = jieba.cut(text, cut_all=False) print(" / ".join(seg_list)) # 输出: 某明星 / 新电影 / 首发 / 预告 / 引发 / 全网 / 热议

freq参数的设定逻辑是相对的:默认词典里常见词的频率大约在 1 万到 10 万之间,自定义词给 1 万基本能保证不被切开。太高会导致误切,比如把“明星新”也合并了。如果你在项目里看到jieba.load_userdict("userdict.txt")的调用,说明作者把词典外置了,那样维护成本更低,不需要改代码,直接往文件里加词就行。

分词之后还有两个细节。一是去掉单字词,单个汉字在聚类中几乎不携带可区分的信息;二是保留“否定词+动词”的组合结构,比如“不支持”“不会去”,单独切开会把语义完全搞反。处理方式可以是提前做规则替换,把常见否定组合换成下划线连接的形式。

stopwords = set() with open('stopwords_cn.txt', encoding='utf-8') as f: for line in f: stopwords.add(line.strip()) def tokenize(text: str) -> list: cleaned = clean_weibo_text(text) words = jieba.cut(cleaned, cut_all=False) result = [] for w in words: w = w.strip() if len(w) < 2: # 去掉单字和空字符串 continue if w in stopwords: # 去掉停用词 continue if w.isdigit() and len(w) > 4: # 去掉疑似时间戳的纯数字 continue result.append(w) return result # 使用:df['tokens'] = df['text'].apply(tokenize)

单字过滤的阈值不是一成不变的,如果聚类结果发现“剧”“片”“人”这类单字频繁出现在多个簇的主题词里,说明它们带信息量,可以单独保留。停用词表的来源很多,GitHub 上有几个经典的中文停用词库,但建议在通用停用词基础上追加一批微博特有词,比如“转发微博”“分享图片”“收起全文”这类结构化冗余内容。

3.3 TF-IDF 的参数不只是默认值:min_df、max_df 和 ngram_range 怎么调

文本变成向量这一步,项目源码里大概率用到了TfidfVectorizer。这个类的参数里,min_dfmax_df是最容易被忽略但影响最大的两个。默认值min_df=1表示所有在语料中出现至少一次的词汇都进入词表,这会导致向量维度极高、噪声极大;微博短文本里很多词语只出现一两次,它们对聚类没有任何帮助。

from sklearn.feature_extraction.text import TfidfVectorizer # 把分词结果重新拼成空格分隔的字符串 df['clean_text'] = df['tokens'].apply(lambda x: ' '.join(x)) vectorizer = TfidfVectorizer( min_df=3, # 至少在 3 篇文档中出现过,过滤长尾噪声 max_df=0.6, # 在超过 60% 的文档中都出现,过滤“全场通用”的泛词 ngram_range=(1, 2),# 词和二元词组都保留 sublinear_tf=True # 对 tf 做 log 变换,弱化高频词优势 ) tfidf_matrix = vectorizer.fit_transform(df['clean_text']) print(tfidf_matrix.shape) # (样本数, 特征数)

关于这组参数,我的做法和经验值是:样本量在几千条时,min_df=3min_df=5是比较稳的区间;样本量过万可以进一步提高到min_df=10max_df=0.6意味着如果一个词出现在 60% 以上的微博里,这个词大概率是“今天”“一个”“真的”这类泛化词,去掉它聚类才会出现区分度。ngram_range=(1, 2)的意义在于“电影首发”这种两个词组合起来才有具体含义的短语,光看单字词可能切碎了语义。调大 ngram 后特征维度会爆炸,如果发现矩阵太大跑不动,优先降 ngram 而不是把min_df调高。

有个常见误用是只调用fit_transform不看中间结果。实际上打印出每类特征的代表词会帮你快速诊断参数是否合理。用下面的代码把向量映射回词语:

feature_names = vectorizer.get_feature_names_out() # 打印 TF-IDF 值最高的 10 个特征,看看它们是否真的有意义 dense = tfidf_matrix.toarray()[0] top_indices = dense.argsort()[-10:][::-1] print([feature_names[i] for i in top_indices])

4. 聚类算法怎么选:KMeans、MiniBatchKMeans 与 LDA 主题模型的对比与实践

4.1 KMeans 是舆情聚类的默认起点,但不是终点

做过文本聚类的人都知道,KMeans 是默认选择——因为它快、可控、有成熟的评估指标。微博热点舆情这个场景更是如此,数据量级通常在几万条以内,特征向量是稀疏的 TF-IDF,KMeans 跑起来非常轻松。项目源码里大概率也是从 KMeans 开始。实现本身不复杂:

from sklearn.cluster import KMeans from sklearn.metrics import silhouette_score # 用肘部法则先粗选 K 值范围 k_range = range(2, 15) scores = [] for k in k_range: km = KMeans(n_clusters=k, random_state=42, n_init=10) labels = km.fit_predict(tfidf_matrix) score = silhouette_score(tfidf_matrix, labels) scores.append(score) print(f"K={k}, silhouette_score={score:.4f}") # 选择轮廓系数最高的 K best_k = k_range[scores.index(max(scores))] print(f"最佳 K 值: {best_k}")

轮廓系数的取值逻辑要讲清楚:范围从 -1 到 1,越接近 1 说明簇内紧凑、簇间分离;0 附近说明样本在两个簇的边界上。实际舆情数据中,轮廓系数在 0.15 到 0.3 之间已经算可用,如果超过 0.4 反而要怀疑是不是数据人工构造的。文本聚类不是类别明确的分类任务,微博话题之间天然有交叉。

n_init=10这个参数容易被忽略。KMeans 的初始质心是随机的,n_init表示用 10 组不同初始质心分别迭代、取最优结果。默认值是 10,但如果数据量大,可以降到 5 提升速度。random_state=42是为了结果可复现,这对调试很重要——如果没有固定随机种子,同一份代码每次跑出来的聚类结果都不同,排查问题时你会疯掉的。

4.2 数据量大时换 MiniBatchKMeans:参数怎么调不降质

微博数据如果做了一个月以上的持续采集,或者热点话题的所有相关微博都抓全,数据量很可能超过几十万条。这时候 KMeans 每次迭代都要用全量数据计算距离,内存占用和耗时会明显上升。MiniBatchKMeans 是 KMeans 的近似实现,每次随机抽一个小批量样本来更新质心,训练速度提升明显,代价是聚类结果有一点随机波动。

from sklearn.cluster import MiniBatchKMeans mbk = MiniBatchKMeans( n_clusters=best_k, batch_size=1024, # 每批样本数,越大越接近标准 KMeans n_init=3, # 初始化次数可以比 KMeans 少,因为有批量随机性 max_iter=100, random_state=42 ) labels_mbk = mbk.fit_predict(tfidf_matrix)

batch_size是 MiniBatchKMeans 最重要的旋钮。设置过小(比如 256)结果波动大;设置过大(接近全量)又失去加速意义。经验值是样本总量的 1% 到 5%,同时不要小于 512。max_iter=100是最大迭代轮数,MiniBatch 收敛速度快,通常 50 轮内就稳定了。这个算法有个特点:如果数据本身就存在一个明显的主簇,MiniBatch 容易把其他小簇合并掉。建议对比标准 KMeans 和 MiniBatchKMeans 在少量样本上的轮廓系数差异,如果差别超过 0.03 就值得考虑用标准版。

4.3 LDA 主题模型是聚类的“另一条路”:它给的不是分组,是主题分布

LDA 不是聚类算法,但它能解决舆情分析里的一个核心诉求——热点话题分组后,每个簇到底在聊什么。KMeans 给出的每个簇的质心向量可以按特征权重大小输出关键词,但主题词经常重叠;LDA 则是为每篇文章计算一个主题分布,然后你可以按主题归属来“软聚类”。

使用 LDA 需要考虑两个预处理差异:一是 LDA 对高频词敏感,停用词要加得更狠;二是特征一般只用CountVectorizer不用 TF-IDF,因为 LDA 本身通过统计共现来发现主题,TF 的权重分布会干扰它的计算。

from sklearn.decomposition import LatentDirichletAllocation from sklearn.feature_extraction.text import CountVectorizer count_vect = CountVectorizer(min_df=5, max_df=0.7) count_matrix = count_vect.fit_transform(df['clean_text']) lda = LatentDirichletAllocation( n_components=8, # 主题个数,对应舆情中的“几个方向” learning_method='batch', # 小数据用 batch,大数据用 online random_state=42, max_iter=20 ) doc_topic = lda.fit_transform(count_matrix) # 打印每个主题的最显著词汇 feature_names = count_vect.get_feature_names_out() for topic_idx, topic in enumerate(lda.components_): top_words = [feature_names[i] for i in topic.argsort()[:-10-1:-1]] print(f"Topic {topic_idx}: {' '.join(top_words)}")

learning_method的选择标准很简单:样本量小于 10 万用batch更稳定,大于 10 万用online提速明显。n_components和 KMeans 的K一样需要调,经验做法是设 5 到 15 之间跑几轮,看每个主题的高频词是否可分。LDA 在短文本上的效果要比长文本差,因为共现信息少,所以如果微博正文普遍很短,LDA 的结果可能不如 KMeans 直观。项目里也可以用 LDA 做 KMeans 之后的簇内再分析,这样两个方向各司其职。

5. 舆情聚类分析常见问题避坑:现象、原因和解决办法

5.1 聚类结果“一坨”:所有话题全挤在一个簇里

现象:不管 K 值设多少,轮廓系数都在 0.05 以下,每个簇的大小差距悬殊,最大簇占了 80% 以上的样本。

原因:多数情况是向量化之前的文本预处理出了问题。最常见的是停用词表太弱,“今天”“真的”“一个”“什么”这类词没有过滤干净,它们在所有文本中频繁出现,成了聚类的主导信号;其次是把转发前缀“转发微博”保留了下来,这类结构化的冗余内容会在 TF-IDF 里获得较高权重。

解决:先返回向量化步骤,把max_df从 0.6 降到 0.3 再跑一次轮廓系数。如果结果没有改善,打印 TF-IDF 特征权重 TOP 20,逐个看这些词是不是真的和“某个方向”相关。再不行就回退检查清洗规则——clean_text字段里是否残留了乱码或英文。舆情聚类的文本质量门槛远高于你的直觉,宁可少留词也不能留泛词。

5.2 Jieba 把关键实体切碎了

现象:聚类主题词里出现“电影”“首发”“预告”而不是“某电影首发预告”,一个作品名被拆成三、四段,导致本来应该聚在一起的话题被分散到不同簇。

原因:默认词典没有收录这个实体名称。我之前碰到过“数据要素”被切成“数据/要素”,聚类结果里数据政策的讨论散落到了三个簇。

解决:最直接的办法是维护一个用户词典。从热点榜拉出全部话题词,再结合当日微博热门搜索词,筛出长度大于等于 2 的专有名词,作为词典输入。jieba.add_wordfreq设 10000 到 20000 之间,太大容易误合并相邻词。需要提醒的是,不要把所有热门词都塞进词典,像“今天”这类高频泛词加进去了反而会影响 jieba 的标准分词效果。

5.3 编码问题导致运行到一半崩溃

现象:读入 CSV 时直接报UnicodeDecodeError,或者聚类跑了很久之后发现大量文本变成了\uXXXX转义字符。

原因:微博数据的编码有两种常见情况——CSV 可能是gbkutf-8混合;JSON 字段里嵌了\u转义但没有被正确解码。有些爬虫把数据写文件时用了默认编码,换到 Linux 环境读取就出问题。

解决:读文件时统一用utf-8尝试,报错就回退到gbk,用errors='ignore'不推荐,因为会静默丢数据。另一个更稳妥的方案是所有中间结果存成parquetjsonl格式,这两个格式的编码问题比 CSV 少得多。

import pandas as pd def load_csv_auto(path: str): """按编码优先级自动加载,并输出实际使用编码""" for enc in ['utf-8-sig', 'gbk', 'latin1']: try: df = pd.read_csv(path, encoding=enc) print(f"File loaded with encoding: {enc}") return df except UnicodeDecodeError: continue raise ValueError(f"All encodings failed for {path}")

5.4 聚类结果不稳定:同一份代码两次运行簇标签完全不同

现象:固定了random_state=42之后,KMeans 的标签仍然每次不同,或者不同机器上跑完全不同的簇划分。

原因:如果你在TfidfVectorizer之前对 DataFrame 做了一次sample(frac=1)打乱操作,且没有固定随机种子,那么每次跑的顺序不同,fit_transform生成的稀疏矩阵行序不同,KMeans 的结果虽然理论上不受行序影响,但 MiniBatchKMeans 的批量采样会受到牵连。另一个原因是 numpy 或 sklearn 版本差异导致 SVD 结果有微小浮点差异,聚类边界样本归属变化。

解决:全流程固定所有随机种子,包括 pandas 的抽样、jieba 的分词不涉及随机但要在导入顺序上稳定,还有 sklearn 各估计器的random_state。另外,在聚类完成之后,把df的原始索引作为唯一标识输出到结果文件,下一次跑的时候对比一下哪些样本的簇归属变了,用这种方法定位是预处理波动还是算法波动:

import numpy as np np.random.seed(42) # 全局固定随机种子

5.5 可视化时词云全是废话

现象:词云的 TOP 词是“一个”“没有”“什么”之类的泛词,热点事件的核心词反而出现率很低。

原因:词云脚本直接用了原始文本做词频统计,没走清洗和停用词流程。这和聚类共用一套清洗逻辑,但词云对停用词的敏感度更高——聚类时出现一两次的噪声词对向量影响有限,在词云里却会占据可观的视觉面积。

解决:建一套“词云专用停用词表”,在通用停用词基础上追加人称代词、时间副词、程度副词。词云直接用df['tokens']里过滤后的词做Counter统计。更省事的方法是词云前先跑一遍聚类,只取目标簇的样本做词云,这样词云内容天然聚焦在特定话题方向,视觉表达更有说服力。

6. 让结果能交代:轮廓系数验证、可视化输出与业务解读优化

聚类跑完只是第一步,结果要能给别人讲清楚,才算是真的交付。面对非技术背景的业务方或评审老师,一张散点图加几个簇主题词的组合,比任何评估指标都有说服力。TSNE 降维是业界最常用的可视化手段,把高维 TF-IDF 向量压到二维平面,再用不同颜色标记簇归属,一眼就能看到聚类是否形成了团聚结构。

from sklearn.manifold import TSNE import matplotlib.pyplot as plt # 限制样本量,TSNE 计算复杂度高,全量数据跑很慢 sample_df = df.sample(n=2000, random_state=42) sample_labels = labels_mbk[sample_df.index] # 假设用 MiniBatchKMeans 的标签 tsne = TSNE(n_components=2, random_state=42, perplexity=30, max_iter=300) coords = tsne.fit_transform(tfidf_matrix[sample_df.index]) plt.figure(figsize=(12, 8)) scatter = plt.scatter(coords[:, 0], coords[:, 1], c=sample_labels, cmap='tab20', s=10, alpha=0.7) plt.colorbar(scatter) plt.title("TSNE Visualization of Weibo Topic Clusters") plt.savefig("cluster_result.png", dpi=150, bbox_inches="tight")

TSNE 的参数里,perplexity影响可视化的局部结构,取值范围一般设在 5 到 50 之间,2000 个样本时 30 是一个合理的起点。max_iter=300不够时会出现警告,提高到 500 到 1000 可获得更稳定的嵌入结果。注意 TSNE 每次运行结果会有细微差异,这是正常现象,不要因为这个去调随机种子之外的东西。

可视化之后,还需要一套“簇主题词+样本示例”的输出模块,让非技术人员能快速理解每个簇在讲什么:

def describe_cluster(topic_matrix, labels, df, top_n=10): """为每个簇输出关键词和代表性微博""" results = {} for cluster_id in sorted(set(labels)): # 找到该簇的样本索引 indices = [i for i, lab in enumerate(labels) if lab == cluster_id] if not indices: continue # 用 cluster 内词频最高的词作为主题词 cluster_texts = [df['clean_text'].iloc[i].split() for i in indices] from collections import Counter word_counter = Counter() for tokens in cluster_texts: word_counter.update(tokens) top_words = [w for w, _ in word_counter.most_common(top_n)] results[cluster_id] = { "size": len(indices), "top_words": top_words, "examples": df['text'].iloc[indices[:3]].tolist() } return results cluster_summary = describe_cluster(tfidf_matrix, labels_mbk, df) for cid, info in cluster_summary.items(): print(f"簇 {cid}: 共 {info['size']} 条样本,主题词: {'、'.join(info['top_words'])}")

这段代码输出的“大小+主题词+样例微博”就是舆情报告的核心素材。运营人员在解读时会发现,具体哪条微博属于哪个簇并不重要,重要的是“哪些话题在抱团”“哪些话题只是孤零零发酵”。这部分逻辑如果项目源码里有,可以直接套用;如果没有,按上面的结构补上就行。

最后说一个我自己的习惯:舆情聚类的项目交付时,建议做一份“时间切片对比”。把数据按天或按小时切片,分别跑聚类,观察簇的数量和主题词随时间变化的轨迹。某话题从“单个热点”变成“簇”再裂成“子话题”的过程,才是舆情分析里最有价值的信息。我接手过的类似项目里,那些让业务方真正觉得有用的结论,都不是看完静态聚类结果产生的,而是看了时间维度上簇的变化。这是这套源码和数据之外,你可以额外投入一点精力获得的增量。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/24 1:00:21

基于SSM框架的农产品电商系统开发实践

1. 项目概述&#xff1a;基于SSM的助农特色农产品销售系统作为一名深耕Java领域多年的开发者&#xff0c;我最近完成了一个具有社会价值的毕业设计项目——基于SSM框架的助农特色农产品销售系统。这个系统专为解决农产品销售渠道单一、信息不对称等问题而设计&#xff0c;通过数…

作者头像 李华
网站建设 2026/9/24 0:58:30

Python零基础转型:首日高效学习框架与实战

1. 从零开始的Python转型之路作为一名从传统行业转投Python开发的"新生代程序员"&#xff0c;我清楚地记得第一天接触这门语言时的困惑与兴奋。Python以其简洁优雅的语法和强大的生态系统&#xff0c;成为技术转行者的首选语言。但真正开始学习时&#xff0c;面对海量…

作者头像 李华
网站建设 2026/9/24 0:56:49

RPA自动化解放生产力:影刀实战经验分享

1. 项目背景与核心价值去年接手新项目时&#xff0c;我每天要花3小时重复处理Excel报表。直到发现影刀RPA这个神器&#xff0c;才真正体会到"科技解放生产力"的含义。现在我的日报生成、数据核对、邮件发送等重复工作全部交给机器人处理&#xff0c;每天多出2小时研究…

作者头像 李华
网站建设 2026/9/24 0:49:08

基于Python的舆情热点分析平台:从网易新闻爬虫到情感可视化

简介&#xff1a;面向Python课程设计与毕业设计的一站式舆情热点分析平台源码&#xff0c;完整覆盖从网易新闻及评论抓取、数据清洗、中文分词、停用词过滤、情感分析、关键词提取到时间序列分析与可视化展示的典型数据科学流程。资源共1403个文件&#xff0c;约23.83MB&#x…

作者头像 李华
网站建设 2026/9/24 0:39:48

基于PCD小样本数据集的PCB元器件缺陷检测:YOLOv8训练与产线落地实践

简介&#xff1a;PCD表面元器件缺陷检测数据集面向从事工业质检、电子制造与目标检测算法实践的开发者与研究者&#xff0c;用于训练和验证PCB表面元器件缺陷识别模型。数据集包含超过600张标注图像&#xff0c;已统一处理为YOLO格式并完成数据增强&#xff0c;可直接用于YOLO全…

作者头像 李华
网站建设 2026/9/24 0:38:16

C++ std::prev详解:告别`--v.end()`的迭代器安全回退

1. 为什么需要这个函数&#xff1a;从*(--v.end())的隐患说起我之前在review同事代码时看到这样一行&#xff1a;auto it --v.end();他当时想拿vector的最后一个元素&#xff0c;这段代码确实能编译、能运行&#xff0c;在std::vector上表现得很好。我当时问了他一句&#xff…

作者头像 李华