news 2026/10/27 18:46:34

Python+百度API实现微博评论情感分析全链路

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Python+百度API实现微博评论情感分析全链路

简介:围绕百度云API与Python实现的微博评论情感偏向分析,这份压缩包面向希望快速上手中文文本情感计算的学习者与课程实践者,提供从评论数据到情感得分、再到倾向判定的可运行示例。包内共15个文件,以3个Python脚本、6个XML配置、2个XLS表格和1个CSV评论数据集为主,另含少量工程说明文件,整体约421KB,结构紧凑,便于按数据、代码、配置模块查阅。已有203人学习下载,适合用作课程作业、小论文实验或舆情分析的入门模板。读者可参考其中的微博评论数据,理解调用百度API获取文字情感得分的流程,并借助标准化脚本把原始得分映射为可比较的实际倾向;XML与工程文件则保留了项目配置线索,方便复现和二次调整。整体侧重数据与脚本的配套示例,能帮助读者快速建立情感分析实验框架,并在此基础上扩展品牌口碑、公共事件舆论等场景的验证。

1. 三万条评论压在一张表里:这条土路为什么还值得走

一个做消费品的 A 同学把后台导出的三万条微博评论数据甩过来,问能不能当天看出这次投放有没有翻车。人工一条条读不现实,逐条问大模型成本顶不住,最后落回最土的一条路:百度API 做情感计算,python 写调用链,微博评论数据先洗后打分。单条成本几乎可以忽略,三万条一天跑完,结果落库还能换口径反复出图。适合手上有几千到几十万条中文短文本、只想要正负向分布的人,也适合刚学 python 想跑通一条完整链路的新手。反讽、方言、缩写它认不准,但你要的是趋势不是逐条判决。下面按实际跑通的顺序拆:返回怎么读、文本怎么洗、调用链怎么写、哪里会翻车。

2. 先读懂返回结构:情感倾向接口给的四个字段各管什么

很多人接上接口就开始跑,跑完导出一列sentiment求平均值,结果发现"正向比例 78%"和自己体感完全对不上。问题多半不在模型,而在字段被误读了。情感倾向分析接口一次调用返回的东西不多,但每个字段的语义边界很清楚,先把这层弄清楚,后面写聚合逻辑才不会返工。

2.1 sentiment 是判决,confidence 是判决的把握

一次正常返回大致长这样,items是数组,单条文本就一个元素:

{ "log_id": "1234567890", "text": "这次活动力度还行吧", "items": [ { "sentiment": 1, "confidence": 0.52, "positive_prob": 0.41, "negative_prob": 0.13 } ] }

sentiment是离散结论,取值 0 负向、1 中性、2 正向。confidence是"这个离散结论有多可信",注意它不等于正向概率,也不等于负向概率,它是模型对最终那一类的把握程度。上面这条,模型判了中性,但把握只有 0.52,说明它自己也在犹豫。这种样本后面要单独拎出来看,不能直接混进正常样本里算比例。

log_id是这次请求的追踪号,出问题找客服或者自查日志时用得上,建议原样落库,别扔。text字段回显的是你传进去的原文,如果你传之前做过截断,回显的就是截断后的内容,可以用来核对到底送进去了什么。

提示:不同接口版本返回的字段名可能略有差异,接之前先在控制台用在线调试工具发一条,把真实返回拷下来对照,比看文档快。

2.2 positive_prob 才是能当强度用的那个

positive_prob和negative_prob是两个独立打分,加起来通常不等于 1,因为中性也占了一部分概率空间。所以拿positive_prob直接当"正向强度"是错的,拿它和negative_prob求比值也不对。

真正好用的连续型得分是自己构造的:

score = positive_prob - negative_prob

范围在 -1 到 1 之间,正数偏正向,负数偏负向,0 附近是中性。这个值的好处是平滑,按天求均值画出来的曲线不会因为模型在临界点上抖动而剧烈跳变。如果你直接用离散的sentiment求平均,一条从 1 跳到 2 的样本会让曲线出现台阶,看不出趋势。

但要清楚这个 score 没有经过概率校准。意思是 score=0.6 不代表"60% 的概率是正向",只代表"在这套打分体系里偏正向偏得比较多"。做横向对比(今天 vs 昨天、这次活动 vs 上次活动)没问题,做绝对断言("67.3% 的用户是正向的")就要谨慎,最好配合抽样校验一起报。

2.3 三条技术路线怎么选

做中文短文本情感分析,摆在面前的基本就三条路。把成本、可控性、维护量摊开对比,选型其实没什么悬念:

路线单条成本冷启动精度上限维护成本适用场景
自训练分类器极低需要几千条标注取决于标注质量高,要持续迭代领域固定、量级上千万、有标注人力
通用大模型逐条问高无需标注高,但输出不稳定中,要写 prompt 和校验样本量小、需要归因解释
云 NLP 接口低无需标注中等,短文本够用极低量大、只要正负向分布

三万条这个量级,走第三条。真要上千万条再考虑自训练,而且自训练的标注集往往还是先用接口跑一遍再人工修,等于绕不开这一层。用通用大模型逐条问的问题不在于贵,而在于输出格式不稳定,你得写解析器兜住各种花式回答,反而不省心。

这条路线的天花板也要认:反讽("这波操作我给满分")、方言("巴适得很")、圈层缩写("yyds""绝绝子")基本都会判错,或者判成中性低置信。所以不要把单条结果当事实用,只把它当统计信号。

2.4 接入前的应用创建与密钥管理

准备工作不复杂,但有两个细节容易埋雷。一是应用要单独创建,别和别的项目共用一个 Key,否则一个项目把日配额跑光,另一个项目当天就瘫了。二是密钥绝对不能写进代码,尤其不能提交到代码仓库。

我一般用环境变量管理,本地开发写进 shell 配置,线上走配置中心或者容器环境变量:

# 写入当前会话,重启终端失效;长期用请写进 ~/.bashrc 或 ~/.zshrc export BAIDU_NLP_AK="你的_API_Key" export BAIDU_NLP_SK="你的_Secret_Key" # 验证是否生效,输出应为你的 AK echo $BAIDU_NLP_AK

AK是 API Key,SK是 Secret Key,两个配合换access_token。代码里统一用os.environ["BAIDU_NLP_AK"]读取,读不到就直接抛异常退出,别给一个空字符串兜底,否则会以"密钥错误"的形式在几百条之后才炸,排查起来很费劲。

控制台里还要确认两件事:情感倾向分析这个能力有没有开通,当前账号的免费额度和 QPS 限制是多少。QPS 是每秒请求数,免费额度是每天的总调用量,这两个数字决定了后面限流参数怎么设,一定要在控制台页面上亲眼看到,别按网上教程里的老数字来配。

3. 微博文本清洗:把噪音剥掉,留下能进接口的那 2000 字节

接口对text有字节上限,超出直接报参数错误;同时微博原文里塞满了话题标签、转发链、表情占位符、零宽字符,这些东西送进去只会稀释语义。清洗这一步做得好不好,直接决定最终分布可不可信,比换个模型影响大得多。

3.1 微博文本里的六类噪音

先把噪音分类,才知道该按什么顺序处理:

噪音类型典型形态处理方式
网页链接https://...、O网页链接整体删除
转发链//@某某: 原话删除整段,重复执行
@提及@某某某删除,保留后面正文
话题标签#某某话题#去掉井号,保留话题文字
表情占位符[微笑]、[二哈]删除,接口不认
控制字符\u200b、\ufeff、私有区图标删除

话题标签我建议保留文字而不是整段删掉,因为话题词本身往往就是评论的主题,比如#新品发布#里的"新品发布"对判断情感是有信息量的,删干净反而丢了上下文。表情占位符则相反,[微笑]在中文语境里甚至是反讽信号,但接口读不懂方括号里的词,留着只会干扰分词,删掉更干净。

另外还有一类不是噪音但会污染统计的:同一条内容被大量转发。转发链清理之后,几十条转发会变成几十条一模一样的文本,等于给这条内容加了权重。量级大、要做分布统计时,建议按clean_text去重后再计数,或者至少单独统计重复度。

关于数据来源,只处理公开可见的内容,不采集用户身份、手机号、位置这类信息,采集频率压到最低,落库时把昵称、主页链接这类字段直接丢掉,只留文本和时间。这既是合规底线,也让后面的表结构干净很多。

3.2 清洗顺序与实现

顺序有讲究:必须先把 HTML 实体反转义、把控制字符干掉、把链接删掉,再去处理转发链和 @,因为链接里经常夹带@和#,先删 @ 会把链接切成碎片残留在文本里。转发链要跑两次,因为//@A: //@B: 原文这种嵌套结构,一次正则只能剥掉最外层。

import html import re RE_URL = re.compile(r"https?://\S+|O网页链接|网页链接") RE_REPOST = re.compile(r"//\s*@[^::\s]{1,30}\s*[::]?") RE_AT = re.compile(r"@[A-Za-z0-9_\-\u4e00-\u9fa5]{1,30}") RE_TOPIC = re.compile(r"#([^#\n]{1,40})#") RE_EMOJI = re.compile(r"\[[\u4e00-\u9fa5A-Za-z]{1,6}\]") RE_CTRL = re.compile(r"[\u200b-\u200f\u2060\ufeff\ue000-\uf8ff]") RE_SPACE = re.compile(r"[ \t\u3000]+") def clean(raw: str) -> str: """把一条微博原文压成可送进情感接口的纯文本""" s = html.unescape(raw or "") # 1. 先干掉零宽字符和私有区图标,否则后面正则的边界会判错 s = RE_CTRL.sub("", s) # 2. 链接优先删,链接内部可能带 @ 和 # s = RE_URL.sub(" ", s) # 3. 转发链跑两次,处理 //@A: //@B: 这种嵌套 s = RE_REPOST.sub(" ", s) s = RE_REPOST.sub(" ", s) # 4. @ 提及删掉,正文保留 s = RE_AT.sub(" ", s) # 5. 话题保留文字,井号去掉 s = RE_TOPIC.sub(r"\1", s) # 6. 表情占位符删掉 s = RE_EMOJI.sub(" ", s) # 7. 收尾:合并空白、去掉首尾标点 s = RE_SPACE.sub(" ", s) return s.strip(" ,。,.!?!?~~\n\t")

RE_EMOJI里限制 1 到 6 个字符是有意的,真人昵称里的方括号内容可能很长,限制长度能避免误伤正文。RE_CTRL里的\ue000-\uf8ff是私有使用区,微博客户端的一些自定义表情就是渲染成这个区间的字符,肉眼看不见但会占字节、干扰分词。

3.3 字节截断与无效样本过滤

接口按 UTF-8 字节数限长,不是字符数。一个汉字 3 字节,一条 800 字的微博就是 2400 字节,直接超限。所以截断必须按字节做,并且要保证截断后不出现半个汉字。

def clip_utf8(s: str, max_bytes: int = 2000) -> str: """按 UTF-8 字节截断,尽量收在句读处""" b = s.encode("utf-8") if len(b) <= max_bytes: return s # errors="ignore" 会自动丢掉末尾被切断的半个字符 cut = b[:max_bytes].decode("utf-8", errors="ignore") # 从后往前找一个句读,避免把"不""没"这类否定词和后面的词切开 for ch in ("。", "!", "?", ";", ",", " "): idx = cut.rfind(ch) if idx > 0 and len(cut[:idx].encode("utf-8")) >= max_bytes // 2: return cut[:idx] return cut

max_bytes取 2000 而不是贴着接口上限,是留一点余量给编码差异和后续可能的字段拼接。截断位置优先找句读,是为了避免把"这个不行"切成"这个不",语义直接反转,这种错误在统计里看不出来,只能靠截断逻辑提前防住。

清洗完之后还要过滤一批无效样本:长度小于 4 个字符的("哈哈哈""转发微博"这类)、纯标点或纯数字的、清洗后变成空串的。这些送进去只会得到低置信的中性结果,白白消耗额度,还会把中性比例抬高。过滤规则直接写在入库前的判断里,别指望事后补。

3.4 落库表结构

表结构决定后面能不能断点续跑、能不能回溯。最小可用的设计是两个关键点:一是原文和清洗后文本都留,出问题时能对比;二是状态字段和唯一索引,保证重跑不重复、不丢数。

-- SQLite 版本,MySQL 需把 AUTOINCREMENT 改成 AUTO_INCREMENT CREATE TABLE weibo_comment ( id INTEGER PRIMARY KEY AUTOINCREMENT, comment_id TEXT NOT NULL, -- 平台侧的唯一标识 batch_tag TEXT NOT NULL, -- 采集批次,区分不同次抓取 raw_text TEXT NOT NULL, -- 原始文本,回溯用 clean_text TEXT NOT NULL, -- 清洗后文本,送接口的就是它 publish_time TEXT, -- 发布时间,聚合按天要用 like_count INTEGER DEFAULT 0, -- 点赞数,可做加权 status INTEGER DEFAULT 0, -- 0待处理 1成功 2参数错 3异常 sentiment INTEGER, -- 0负向 1中性 2正向 confidence REAL, positive_prob REAL, negative_prob REAL, log_id TEXT, err_msg TEXT, updated_at TEXT ); CREATE UNIQUE INDEX uk_comment ON weibo_comment(batch_tag, comment_id); CREATE INDEX idx_pending ON weibo_comment(batch_tag, status, id);

唯一索引建在(batch_tag, comment_id)上而不是单独的comment_id,因为同一条微博可能在不同批次被重复采到,你要允许它存在两次,但不能在同一个批次里重复。idx_pending是给取待处理数据用的,status=0的扫描会走这个索引,几十万行也不会慢。

like_count建议留着。后面做加权聚合时,一条一万赞的评论和一条零赞的评论权重显然不该一样,但怎么加权是业务判断,先把数据存下来,口径可以后面再换。

4. 调用链落地:token 缓存、限流退避与断点续跑

清洗好的数据要一条条送出去。这一段最容易出问题的地方,不是调用本身,而是 token 管理、限流节奏和失败恢复。写得糙一点,跑三万条能把你折腾一整天。

4.1 access_token 的缓存与刷新

access_token是拿 AK/SK 换的,有效期通常按天算。最常见的错误写法是每处理一条就换一次 token,请求量直接翻倍,还没开始调接口就把 QPS 打满了,然后满屏的错误码。

正确做法是进程内缓存,用一个带锁的类管住,临近过期才刷新:

import os import time import threading import requests TOKEN_URL = "https://aip.baidubce.com/oauth/2.0/token" class TokenKeeper: """线程安全的 token 缓存,提前一天刷新""" def __init__(self, ak: str, sk: str, refresh_ahead: int = 86400): self.ak = ak self.sk = sk self.refresh_ahead = refresh_ahead # 提前刷新的秒数 self._token = None self._expire_at = 0.0 self._lock = threading.Lock() def get(self, force: bool = False) -> str: with self._lock: now = time.time() if not force and self._token and now < self._expire_at: return self._token resp = requests.get(TOKEN_URL, params={ "grant_type": "client_credentials", "client_id": self.ak, "client_secret": self.sk, }, timeout=10) resp.raise_for_status() data = resp.json() if "access_token" not in data: raise RuntimeError(f"取 token 失败: {data}") self._token = data["access_token"] # expires_in 单位秒;提前 refresh_ahead 秒视为过期 self._expire_at = now + data["expires_in"] - self.refresh_ahead return self._token

三个参数值得说:refresh_ahead设成 86400(一天),意味着在真正过期前一天就换新的,避免跑到一半 token 失效;force参数是给错误码 110/111 用的,遇到 token 失效时强制刷新一次再重试;_lock保证多线程下只有一个线程去换 token,其他线程等结果,不会出现并发换 token 把配额打光。

AK/SK 从环境变量读,别硬编码:

keeper = TokenKeeper( ak=os.environ["BAIDU_NLP_AK"], sk=os.environ["BAIDU_NLP_SK"], )

4.2 单条调用的封装与错误码分流

接口请求本身简单,难在错误码怎么分类。我的习惯是分四档:可重试的、要换 token 的、配额用尽的、参数错的。四档处理方式完全不同,混在一起写就会出现"参数错误也重试四次"这种浪费。

import random import requests SENTIMENT_URL = "https://aip.baidubce.com/rpc/2.0/nlp/v1/sentiment_classify" RETRYABLE = {18, 282000, 282810} # QPS 超限、服务内部错误等 TOKEN_INVALID = {110, 111} # token 无效 / 过期 QUOTA_EXHAUSTED = {17, 19} # 日配额或权限用尽 def _call_once(text: str, token: str, timeout: int = 8) -> dict: resp = requests.post( SENTIMENT_URL, params={"access_token": token, "charset": "UTF-8"}, json={"text": text}, # 用 json= 让 requests 自行处理 UTF-8 timeout=timeout, ) resp.raise_for_status() return resp.json() def analyze(text: str, keeper: TokenKeeper, max_retry: int = 4) -> dict: """返回 items[0],并把 log_id 塞进去方便落库""" for attempt in range(max_retry + 1): token = keeper.get() try: data = _call_once(text, token) except requests.RequestException: # 网络层错误:退避后重试 if attempt == max_retry: raise time.sleep(min(2 ** attempt + random.random(), 30)) continue code = data.get("error_code", 0) if code == 0: item = data["items"][0] item["log_id"] = data.get("log_id", "") return item if code in TOKEN_INVALID: keeper.get(force=True) # 强制换 token,不消耗重试次数 continue if code in RETRYABLE: time.sleep(min(2 ** attempt + random.random(), 30)) continue if code in QUOTA_EXHAUSTED: raise RuntimeError(f"配额用尽,停止: {data}") # 参数类错误不重试,交给上层标记跳过 raise ValueError(f"不可重试错误 {code}: {data.get('error_msg')}") raise RuntimeError("重试次数耗尽")

json={"text": text}这个写法比data=json.dumps({"text": text})更稳,因为 requests 会自己把Content-Type设成application/json并做 UTF-8 编码,省掉一类编码问题。退避用的是2 ** attempt + 随机数,最大封顶 30 秒,随机数是为了避免多个线程同时重试形成脉冲。

QUOTA_EXHAUSTED直接抛出中断整个任务,不要吞掉继续跑。配额用尽之后每一条都会失败,继续跑只是往库里写一堆无意义的错误记录,还会把状态字段搞乱。

4.3 并发控制:QPS 从 1 开始压

免费额度的 QPS 通常很低,一上来开十个线程必然满屏错误码 18。稳妥的做法是单线程匀速跑,或者用线程池加信号量把并发压住。我的习惯是先按 1 QPS 跑一百条,看有没有错误,再逐步往上加,最终稳定在控制台标注值的 0.8 倍左右。

单线程匀速版本最省心,也最不容易翻车:

import time def run_batch(conn, keeper, batch_tag, qps=1.5, page_size=200, limit=None): """从 status=0 的记录里取数,逐条调用并回写。Ctrl-C 后重跑可续""" interval = 1.0 / qps done = 0 while True: rows = conn.execute(""" SELECT id, clean_text FROM weibo_comment WHERE status = 0 AND batch_tag = ? ORDER BY id LIMIT ? """, (batch_tag, page_size)).fetchall() if not rows: break for row in rows: t0 = time.time() try: item = analyze(row["clean_text"], keeper) conn.execute(""" UPDATE weibo_comment SET status = 1, sentiment = ?, confidence = ?, positive_prob = ?, negative_prob = ?, log_id = ?, updated_at = datetime('now') WHERE id = ? """, (item["sentiment"], item["confidence"], item["positive_prob"], item["negative_prob"], item.get("log_id", ""), row["id"])) except ValueError as e: # 参数类错误:标记跳过,不阻塞后续 conn.execute( "UPDATE weibo_comment SET status=2, err_msg=? WHERE id=?", (str(e)[:200], row["id"])) except Exception as e: # 网络/未知异常:标记 status=3,下一轮人工或脚本重试 conn.execute( "UPDATE weibo_comment SET status=3, err_msg=? WHERE id=?", (str(e)[:200], row["id"])) conn.commit() done += 1 cost = time.time() - t0 if cost < interval: time.sleep(interval - cost) if limit and done >= limit: return done return done

参数说明:qps=1.5是每秒请求数,第一次跑务必从 1 开始;page_size=200是每批取多少行,取太大内存占得多、也更容易在某条失败时丢掉进度;limit用来做小样本试跑,传 100 就只跑一百条,验证通了再放开。

conn.commit()放在单条循环内而不是每批结束,看起来费,但这是断点续跑的关键:任何时候 Ctrl-C 或者进程被杀,已完成的记录都已落库,重跑时status=0的查询自然跳过它们。用 SQLite 时这个提交开销可以忽略,MySQL 的话建议改成每 50 条提交一次。

4.4 断点续跑与失败重试

上面这套状态机是 0 待处理、1 成功、2 参数错、3 异常。重试只需要把 3 拨回 0:

-- 把异常记录重新放回待处理队列,参数错的不要动 UPDATE weibo_comment SET status = 0, retry_count = retry_count + 1 WHERE batch_tag = ? AND status = 3 AND retry_count < 3;

retry_count < 3是个闸门,防止某条数据因为特殊字符一直失败,被反复送进队列形成死循环。跑够三次还失败的,多半是真的有脏数据,人工捞出来看一眼比继续重试划算。

跑完一批之后一定要对账,三个数必须能对上:

SELECT status, COUNT(*) FROM weibo_comment WHERE batch_tag = ? GROUP BY status;

status=1的数量加上 2 和 3 的数量,应该等于status非空的全部行数。如果对不上,说明有记录根本没被读到,多半是batch_tag写错了或者清洗阶段就没入库。

5. 踩坑排查:五个把结果带偏的细节

这一段是我自己在不同项目里反复撞过的墙,按现象、原因、解决写,遇到对得上的直接照方抓药。

5.1 中性比例高得离谱,正向负向都很少

现象:跑完十万条,中性占 71%,正向 16%,负向 13%,但业务侧反馈"这次明明骂声一片"。

原因:清洗过度,很多评论清洗完只剩几个字甚至空串,或者残留的[微笑]这类占位符还在。短到"还行""不错"这种两三个字的样本,接口本身就容易判中性低置信。

解决:先抽样看clean_text,随机拉 50 条肉眼过一遍,看看是不是被削得太狠。把最小长度过滤从 4 调到 3 或者干脆不过滤,同时把confidence < 0.55的样本单独统计成一个"判不准"桶,不要混进中性里。这一步做完,中性比例通常会降 10 个百分点以上。

5.2 跑到几百条后全部失败,错误码 18

现象:前 300 条正常,之后每条都返回 QPS 超限,重试也没用,越重试越糟。

原因:两个可能叠加。一是 token 每条都重新取,请求量翻倍;二是重试没有退避,失败后立刻重发,把本来就紧张的配额彻底打满。

解决:确认TokenKeeper真的在做缓存(打印一下_expire_at看有没有变);重试必须带指数退避;把 QPS 降到 1 重新跑。如果控制台显示的是共享配额,还要确认同账号下没有别的程序在跑。

5.3 中断重跑后数据翻倍

现象:脚本跑到一半断了,重跑一遍,最后统计出来的总量是预期的一点五倍。

原因:表上没有唯一约束,重复插入;或者采集阶段就没去重,同一条微博被多个批次抓进来,而聚合时没按batch_tag过滤。

解决:唯一索引建在(batch_tag, comment_id)上,插入用INSERT OR IGNORE(MySQL 用INSERT IGNORE)。聚合查询永远带batch_tag条件,别图省事全表扫。

5.4 超长微博被判成完全相反的情感

现象:一条长评论原文明显是差评,结果判成中性甚至正向。

原因:截断截在了句子中间,把"但是质量真的不行"这后半句切掉了,只留下前面的客套话;或者按字符数截断,UTF-8 字节超限,接口只处理了前面一小段。

解决:按字节截断,并且从后往前找句读落点;对于超过 1500 字节的长文本,可以在截断前先做一次分句,取情感词密度最高的那一段送进去。这条没有银弹,长文本本身就是这类接口的弱项,报告里要注明长文本占比。

5.5 分布和体感差很远,但代码没报错

现象:全流程跑通,没有一条报错,但结果就是和运营的直觉对不上。

原因:样本偏差。常见的是只采了热门评论(排序靠前的天然争议大)、只采了带图的(图文内容情感更极端)、或者只采了某个时间段(活动刚爆发那两小时)。代码没错,错在采样。

解决:采集阶段就按时间分层,每小时抽固定条数;跑完先做一次抽样校验(下一章讲);报告里必须写清楚样本口径——采了多少条、去重后多少条、时间范围、过滤掉了哪些。口径写清楚,即使数字不好看,结论也是站得住的。

6. 从分数到结论:阈值标定与两百条抽样校验

接口跑通只是拿到原料,能不能出结论,取决于你敢不敢给结果标一个误差范围。我现在的固定动作是:先标阈值,再抽样校验,最后才出图。

阈值标定解决的是"哪些样本算数"。把confidence分层统计,你会发现低置信段几乎全是短文本、反讽和圈层黑话。我的做法是设一条线,比如confidence >= 0.6记为可靠样本,低于这条线的单独归入"判不准"桶,只报比例不报倾向。这个桶本身很有价值——判不准的比例突然升高,往往说明这波讨论里反讽和阴阳怪气变多了,这比正负向比例更能说明舆情在变味。

抽样校验解决的是"结果准不准"。从可靠样本里随机抽 200 条,人工标注正负中,和接口结果做混淆矩阵。200 条是个经验值,少于 100 条置信区间太宽,多于 500 条人工成本吃不消。下面的数字是示意,不是标准答案,你要用自己的数据重新跑:

分层样本数人工一致率主要错法
confidence ≥ 0.81200.91反讽被当正向
0.6 ≤ confidence < 0.8600.78中性被判成正/负
confidence < 0.6200.55基本随机

这张表出来之后,报告里就可以写"在置信度 0.8 以上的样本中,人工复核一致率 91%",比单说一个"准确率 85%"可信得多。低于 0.6 那档一致率接近抛硬币,就别往结论里掺了。

聚合口径我用加权得分按天算:

# score 落在 [-1, 1],正数偏正向;按天求均值再画曲线 df["score"] = df["positive_prob"] - df["negative_prob"] daily = (df[df["confidence"] >= 0.6] .groupby(df["publish_time"].str[:10])["score"] .agg(["mean", "count"]) .rename(columns={"mean": "avg_score", "count": "n"})) # 样本量太小的天不要画,否则曲线尖刺全是噪音 daily = daily[daily["n"] >= 30]

n >= 30这个门槛是经验值,一天的样本少于 30 条,均值波动太大,画上去只会让人误判。真要看那几天,就在图上标出来说明样本不足,别硬画。

一个具体技巧:把"判不准比例"和"平均分"画在同一张图的双轴上。平均分告诉你情绪往哪边走,判不准比例告诉你这个判断有多虚。两条线同时抬头,往往意味着争议在扩大,而不是单纯的负面增加。

血泪经验是有一回赶时间,三万条跑完直接出了张"正向占比 82%"的图,会上被问了一句"你抽了几条看",当场卡住。后来补抽 200 条,发现大量反讽被算成正向,真实正向不到 60%。从那以后我每次跑完都先抽 30 条肉眼过一遍再出图,这一步花不了十分钟,但能省掉一次被打脸的会。希望帮到你。

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

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

副队长大数据教程(6)--四节点分布式集群搭建

文章目录1. 任务背景2. 任务规划3. 整体思路4. 共性部分配置4.1 关闭防火墙、关闭SELinux4.2 配置hosts文件4.3 配置网络参数4.4 安装配置JDK4.5 安装配置Hadoop4.5.1 下载Hadoop4.5.2 解压4.5.3 配置环境变量4.5.4 修改hadoop中的JDK配置4.5.5 修改hadoop的从节点配置4.5.6 修…

作者头像 李华
网站建设 2026/10/27 18:46:24

肾脏肿瘤语义分割数据集:预处理、训练与评估避坑指南

简介&#xff1a;面向医学影像分析与深度学习算法研究人员&#xff0c;这是一套已划分好训练/验证集的肾脏肿瘤语义分割数据集&#xff0c;共约2800张影像及对应像素级标签&#xff0c;标注类别涵盖背景、肾脏和肿瘤三类。包内含约2000个文件&#xff0c;以PNG图像和Mask为主&a…

作者头像 李华
网站建设 2026/10/27 18:43:37

泵阀工厂做 Google SEO 和 GEO,产品页面应该如何规划?

对于泵阀制造企业来说&#xff0c;外贸独立站最重要的内容之一就是产品页面。 海外采购商进入网站后&#xff0c;通常需要依次判断&#xff1a;产品类型是否符合需求、规格是否适用、企业能否提供对应资料&#xff0c;以及后续应该如何询价。 如果页面只展示产品图片和几句通用…

作者头像 李华
网站建设 2026/10/27 18:43:34

WDG看门狗

WDG简介WDG&#xff08;Watchdog&#xff09;看门狗看门狗可以监控程序的运行状态&#xff0c;当程序因为设计漏洞、硬件故障、电磁干扰等原因&#xff0c;出现卡死或跑飞现象时&#xff0c;看门狗能及时复位程序&#xff0c;避免程序陷入长时间的罢工状态&#xff0c;保证系统…

作者头像 李华