4. 数据清洗与预处理实战

在主题热度与市场关注度量化项目中,原始数据往往来自多个渠道:搜索引擎指数、社交平台话题量、新闻转载数、电商评论、论坛帖子等。这些数据在进入指标计算之前,几乎不可能直接使用。缺失值会让趋势线断裂,异常值会把均值拉偏,重复记录会虚增热度,文本噪声则会让分词结果失去意义。本节围绕一个可复现的清洗流程展开,目标是把一份"脏"的原始热度数据整理成可直接用于统计与建模的干净数据集。

先看清数据问题,再动手清洗

假设我们采集到某主题连续 30 天的日度热度记录,字段包括:日期、平台、关键词、讨论量、阅读量、情感倾向文本。加载后先做一次体检,常见问题会集中暴露出来:某些日期讨论量缺失;个别平台出现阅读量为 0 但讨论量上千的矛盾记录;同一日期同一平台因多次抓取产生重复行;文本字段里混入 HTML 标签、表情符号、全角空格和"转发微博"之类的模板话术。清洗不是盲目套函数,而是先定位问题类型,再选择对应处理策略。

import pandas as pd
df = pd.read_csv("topic_heat_raw.csv", parse_dates=["date"])
print(df.shape)
print(df.isna().sum())
print(df.duplicated(subset=["date","platform","keyword"]).sum())
print(df.describe())

这段代码给出三个关键信号:各字段缺失数量、按"日期+平台+关键词"判定的重复行数、数值字段的分布范围。分布范围尤其重要,它能帮助判断异常值是录入错误还是真实爆发。

缺失值:区分"没有发生"与"没有采到"

缺失值不能一律删除或一律填充。讨论量缺失若发生在采集故障日,属于"没有采到",可用前后日插值或同平台同期均值填补;若某平台当天确实没有讨论,缺失应填 0。判断依据可以结合阅读量:阅读量正常而讨论量缺失,多半是采集问题;两者同时缺失,则更可能是该平台当日无数据。对于连续缺失超过 3 天的区段,插值会制造虚假平滑,建议标记为缺失并在后续计算中排除,而不是强行补齐。

df["discuss"] = df.groupby("platform")["discuss"] \
    .transform(lambda s: s.interpolate(limit=3, limit_direction="both"))
df.loc[df["read"] == 0, "discuss"] = df.loc[df["read"] == 0, "discuss"].fillna(0)

这里用分组插值保证不同平台各自补齐,limit=3 限制连续填补长度,避免长段缺失被过度平滑。

异常值:用业务规则加统计边界双重判断

异常值处理最忌只看统计量。某天讨论量突然是平日的 20 倍,可能是真实热点事件,也可能是爬虫重复计数。实践中采用双重判断:先用 IQR 或 3σ 找出统计离群点,再结合业务规则复核。例如"阅读量小于讨论量"在多数平台不合理,属于逻辑异常;"单日讨论量超过该平台月均值的 10 倍且无对应新闻事件"则需人工确认。确认是录入错误的直接修正或置空,确认是真实爆发的保留,但在计算均值类指标时考虑使用中位数或截尾均值。

q1, q3 = df["discuss"].quantile([0.25, 0.75])
iqr = q3 - q1
upper = q3 + 1.5 * iqr
df["is_outlier"] = df["discuss"] > upper
df.loc[df["read"] < df["discuss"], "discuss"] = df["read"]

把逻辑矛盾记录按"讨论量不应大于阅读量"修正,是比直接删除更稳妥的做法,因为它保留了该日期的存在性,不影响时间序列连续性。

重复记录与标准化

重复记录常因多次抓取或跨表合并产生。去重键应包含业务主键,即日期、平台、关键词三者的组合,保留最新抓取时间的那一条。标准化则包括:平台名称统一("微博""新浪微博"合并为"微博")、关键词统一大小写与全半角、日期统一为 ISO 格式、数值字段统一为数值类型。标准化做在去重之前还是之后会影响结果,建议先去重再标准化,避免因名称不一致而漏判重复。

df = df.sort_values("crawl_time").drop_duplicates(
    subset=["date","platform","keyword"], keep="last")
df["platform"] = df["platform"].replace({"新浪微博":"微博","weibo":"微博"})
df["keyword"] = df["keyword"].str.strip().str.lower()

文本噪声与分词

情感倾向文本字段是噪声重灾区。清洗顺序为:去除 HTML 标签与 URL、去除 @用户与话题符号、统一全半角、去除模板话术、再分词。中文分词可选用 jieba,并加载自定义词典加入主题相关专有名词,避免"市场关注度"被切成无意义碎片。分词后过滤停用词与单字,保留有情感或主题指向的词,供后续情感打分或词频统计使用。

import re, jieba
def clean_text(t):
    t = re.sub(r"<[^>]+>|http\S+", "", str(t))
    t = re.sub(r"@\S+|#\S+#", "", t)
    t = t.replace("\u3000", " ").strip()
    t = re.sub(r"转发微博|分享图片", "", t)
    return t
df["text_clean"] = df["text"].map(clean_text)
df["tokens"] = df["text_clean"].map(lambda x: [w for w in jieba.cut(x) if len(w) > 1])

下图概括了本节从原始数据到干净数据集的完整处理链路,以及各环节的判断分支:

主题热度数据清洗与预处理链路 原始热度数据 多平台 / 30 天日度 数据体检 缺失 / 重复 / 分布 去重 + 标准化 业务主键 / 名称统一 缺失值处理 分组插值 / 填 0 异常值 IQR + 规则 异常值判断分支 统计离群 → 业务复核 逻辑矛盾(讨论>阅读)→ 修正 真实爆发 → 保留,均值改用中位数 文本噪声清洗与分词 去 HTML / URL / @用户 / 话题符号 → 统一全半角 去模板话术(转发微博、分享图片)→ jieba 分词 加载自定义词典 → 过滤停用词与单字 → 保留主题词 干净数据集 日期 · 平台 · 关键词 · 讨论量 · 阅读量 · 分词结果

清洗结果如何验收

清洗完成后不能只看代码跑通,要用一组检查项确认数据可用:缺失率是否降到可接受范围(例如关键字段低于 2%);去重后行数是否与"日期×平台×关键词"的理论组合数接近;数值字段是否满足"阅读量≥讨论量≥0";分词结果中是否仍残留明显噪声词。下表给出一份可直接套用的验收清单示例,数值为示例假设,实际以项目数据为准。

检查项检查方法示例阈值不通过时的处理
关键字段缺失率isna().mean()≤ 2%回查采集日志,补采或标记排除
重复记录按业务主键 duplicated= 0保留最新抓取时间记录
逻辑一致性阅读量 < 讨论量 的行数= 0按阅读量修正讨论量
异常值占比IQR 标记比例≤ 3%逐条业务复核,区分错误与真实爆发
文本噪声残留抽样检查分词结果无明显模板词补充正则与停用词表
建议把清洗函数封装为可重复调用的管道,并把每次清洗前后的行数、缺失率、异常值数量写入日志。这样当数据源更新或指标结果异常时,可以快速定位是采集问题还是清洗规则失效。

完成上述步骤后,得到的数据集应满足:时间序列连续、平台口径统一、数值逻辑自洽、文本可分词且噪声可控。这份干净数据集才是后续热度指数计算、趋势对比与关注度建模的可靠输入。清洗环节投入的时间,往往直接决定最终量化结论的可信度。