尧图精选

电商评价数据清洗实战:用pandas处理文本脏数据的完整指南

🕒 发布时间:2026/9/11 16:10:58 📁 来源:尧图网络
做数据清洗这活儿很多人觉得没技术含量不就是删删空值、去去重嘛。但真正接过电商数据、用户行为数据、UGC文本数据的人会知道清洗才是整个分析链路里最耗时间、最考验耐心、也最容易翻车的一环。这次拿“电商用户评价数据”开刀是因为它几乎集齐了文本类脏数据的所有经典形态全角半角混用、emoji表情、HTML残留、默认好评、灌水评论、评分和文字情感互相打脸甚至连用户ID都涉及隐私合规。这一套流程走完你基本就掌握了用pandas处理中文文本数据的完整套路后面的AI文本分析、情感分类、评价关键词挖掘全都建立在这份干净数据之上。这个项目是【PythonAI】系列里2.2.5节的项目实战适合刚学完pandas基础、想通过一个完整案例建立“数据清洗方法论”的读者。不需要你有多高深的理论但需要你跟着动手敲代码因为清洗这件事看十遍不如跑一遍。1. 为什么拿“电商用户评价”做数据清洗项目场景与交付物1.1 评价数据几乎是“脏数据样本间”电商评价数据在数据清洗教学里属于“上帝故意给你设坑”的那种数据集。它同时涵盖结构化字段和非结构化文本每一类字段的脏法还不一样。先看一眼典型的用户评价表长什么样字段大致如下字段名含义常见的脏数据形态review_id评价ID重复导入导致ID重复user_id用户ID需脱敏含手机号样式product_id商品ID空值、格式不统一rating评分1-5字符串“4分”、小数“4.5”、越界值9review_time评价时间混有“2024/1/1”和“2024-01-01”不同格式review_title评价标题大量空值有的直接把正文复制过来review_content评价内容HTML标签、emoji、全半角混用、灌水刷屏is_direct_purchase是否直购“是/否”“1/0”“True/False”三种写法location用户地区缺失、省份城市混填我刚拿到真实业务数据的时候最头疼的不是某一个字段脏而是这些脏法叠加在一起。比如一条评价内容里同时有HTML标签、emoji、全角括号、还带着一堆连续重复的“啊啊啊啊啊”灌水痕迹这就不能靠单一规则解决必须建立一套清洗管道按顺序处理。1.2 这份清洗报告要交付什么很多初学者以为清洗就是把数据弄干净跑完代码就结束。实际上在企业里清洗环节的交付物是报告不是代码。数据团队把清洗规则、清洗前后的质量对比、每条规则的业务依据讲清楚业务方和算法团队才敢放心用这份数据。所以这个项目我定了三个明确交付物一份可复现的清洗脚本别人拿到你的代码跑一遍能还原你的清洗全过程。一份清洗规则说明每一条规则为什么要这么定、删了多少数据、填充了多少数据都要有记录。一份清洗前后对比报告用统计数字展示数据质量的变化比如重复率从多少降到0缺失率从多少降到多少。这也是为什么我在后面专门留了一节讲清洗日志和报告生成——清洗做得再漂亮没报告就等于白做。1.3 字段设计提前为后续分析留好接口这个项目虽然是清洗但我在字段设计上会顺手做一些“为后续分析铺路”的事。比如清洗完文本之后我不会直接丢弃原文本而是保留一列review_content_clean供后续做情感分析、词云、关键词提取时使用再比如我会增加一列review_length清洗后的文本长度这列在后面做异常评论检测、内容质量评估时非常有用。这样做的思路是清洗不是终点而是数据分析和建模的起点。如果你只是把数据弄“干净”却丢了原始信息后面要做特征工程就得倒回去翻原始数据那就尴尬了。2. 环境准备与第一轮数据体检2.1 依赖安装与模拟数据构造这个项目的核心依赖是pandas另外需要numpy做数值处理re做正则文本清洗openpyxl用于导出Excel报告。如果后面要做简单的情感分析可以再加jieba但第一步用不到。pip install pandas numpy openpyxl考虑到很多读者手头没有真实的电商评价数据这里我提供一份模拟数据集的构造代码。你可能会问模拟数据有什么意义我的回答是数据分析的真实难点从来不在“有没有数据”而在“你知不知道数据里可能藏着什么问题”。用模拟数据练手你可以把每一种脏数据问题都主动埋进去然后验证自己的清洗逻辑能不能全部识别出来这比直接拿真实数据被动踩坑效率高得多。import pandas as pd import numpy as np import random random.seed(42) np.random.seed(42) n 1200 review_ids [fR{i:06d} for i in range(1, n 1)] # 埋入各种脏数据空值、重复、HTML、emoji、全角、混合格式时间 contents_raw [ 质量很好值得购买, 一般般吧p没有想象中好/p, 太差了客服态度也不好退货, 物流很慢等了一周才到差评, 这个价格真的划算下次还来, 客服态度很好解答很耐心, 用了几天才来评价效果超出预期, 一般没有想象中好凑合用, 垃圾垃圾垃圾垃圾垃圾, 真的很不错啊哈哈哈哈哈哈哈, ] contents [random.choice(contents_raw) for _ in range(n)] user_ids [fU{random.randint(100000, 999999)} for _ in range(n)] product_ids [random.choice([P1001, P1002, P1003, P1004, P1005]) for _ in range(n)] # 评分故意混入字符串、小数、越界值 ratings_raw [] for _ in range(n): r random.choice([1, 2, 3, 4, 5, 4分, 5分, 4.5, 9, 0]) ratings_raw.append(r) # 时间混入两种格式并埋少量空值 times_raw [] for _ in range(n): t random.choice([ 2024-06-15 12:30:00, 2024/6/15 18:20, 2024-07-01 09:00:00, , ]) times_raw.append(t) df_raw pd.DataFrame({ review_id: review_ids, user_id: user_ids, product_id: product_ids, rating: ratings_raw, review_time: times_raw, review_content: contents, is_direct_purchase: random.choice([是, 否, 1, 0, True, False] * (n // 6)), })代码里我刻意埋了问题评分列混有字符串和小数时间列混有两种格式和空字符串内容列有HTML和emoji直购字段有六种写法。这基本上还原了一份真实系统导出数据的混乱程度。模拟数据造好以后导出成CSV模拟“从业务系统拿到一份原始数据”的场景df_raw.to_csv(ecommerce_reviews_raw.csv, indexFalse)2.2 从 info() 和 describe() 里读出的问题拿到原始数据的第一件事不是写清洗代码而是先体检。我习惯按三个步骤来先看结构再看分布最后看样本。df pd.read_csv(ecommerce_reviews_raw.csv) # 第一步结构 df.info()info()会告诉你每一列的非空数量、数据类型、内存占用。看到rating列显示object而不是int64你就该知道评分字段是文本型的后面必须做类型转换看到review_time列里有空字符串被识别成非空值你也该意识到所谓的“缺失”可能不是NaN而是空串。# 第二步分布 df.describe(includeall)describe()对数值列给出均值、分位数对文本列给出唯一值个数和众数。比如review_content的唯一值个数远小于总行数就说明大量重复评论存在这往往是默认好评或灌水的结果。# 第三步抽样看文本 df[review_content].sample(10, random_state1).tolist()抽样看文本非常重要光靠统计数字你根本看不出HTML标签、emoji、全角符号这些东西长什么样。只有亲眼看到“没有想象中好”这种带标签的评论你才知道正则清洗该写什么模式。2.3 体检结果整理成“问题清单”体检之后我习惯把发现的问题列成一张表这张表就是后面清洗工作的“施工图”。我这次体检发现的问题如下问题编号字段问题描述拟处理方案1review_id可能存在重复IDduplicated()检测后删除2user_id涉及用户隐私脱敏处理3rating类型为object含“4分”、小数、越界值提取数字、越界删除4review_time格式混杂有空字符串统一解析为datetime空值转NaN5review_content含HTML、emoji、全角符号、灌水内容正则清洗6is_direct_purchase六种写法混用映射为统一的“是/否”先把问题清单列出来每清洗一个字段就在清单上打钩这样才不会漏项。这也是清洗工程师和普通写代码的人之间最大的区别前者按清单施工后者想到哪洗到哪。3. 清洗主流程六类脏数据逐个击破3.1 重复值先干掉完全重复再处理近似重复重复数据是评价数据集里最常见的问题来源通常是系统重复导入、用户重复提交、或者是网络重试导致的重复写入。清洗重复值我分两步走。第一步处理完全重复before_count len(df) df df.drop_duplicates() after_count len(df) print(f完全重复删除: {before_count - after_count} 条)drop_duplicates()默认对整行所有列进行比较只要所有字段都一样才算重复。但是实际业务里完全重复往往只是一小部分更常见的是近似重复比如同一个人对同一个商品提交了内容几乎相同但时间不同或者ID不同的两条评价。第二步处理近似重复。我通常按user_id product_id review_content这三个业务含义上的关键字段来判定重复df df.drop_duplicates(subset[user_id, product_id, review_content], keepfirst) print(f近似重复删除: {before_count - len(df)} 条)这里有个细节值得注意keepfirst表示保留重复组中的第一条但“第一条”不一定是最完整的那条。更稳妥的做法是先对评论内容长度排序让内容最长的排在最前面再去重这样保留下来的往往是信息更完整的评价。3.2 缺失值不同字段用不同策略缺失值的处理没有万能公式核心原则就一句话先判断缺失是否有业务含义再决定是删除、填充还是保留。对评价数据集来说我用了三种不同策略。第一review_content为空。很多平台对“用户未填写评价内容”会自动生成“此用户未填写评价”这种空值不能直接删因为评分本身还是有分析价值的。我用固定文案填充并单独标记一列df[review_content] df[review_content].astype(str).str.strip() df.loc[df[review_content].isin([, nan, None, NaN]), review_content] np.nan df[content_missing] df[review_content].isna().astype(int) df[review_content] df[review_content].fillna(此用户未填写评价)第二review_time缺失。评价时间对时间序列分析很重要缺失比例不高时我选择直接删除这几行因为填充一个虚假时间反而会污染后续分析。删除前先记录删除行数写进报告。第三rating缺失或非法。如果评分缺失这条评价的数据价值大打折扣同样选择删除。df df.dropna(subset[review_time, rating])这里我想多说一句为什么不全用填充填充的本质是“用估计值代替缺失值”它适用于数值分布稳定、缺失比例不高、且后续建模对完整度要求高的场景。但对于评价时间这种强业务字段你没法估计“用户是哪天写的评价”强行填充只会制造错误信息。该删就删别心疼。3.3 文本清洗标签、全半角、emoji、不可见字符文本清洗是评价数据里最繁重的一步。中文用户写评价格式自由加上很多平台允许富文本导致HTML标签、emoji、全半角符号一股脑混进评论里。我封装了一个clean_text函数按固定顺序处理import re import unicodedata def clean_text(text): if not isinstance(text, str): return # 1. 去除HTML标签 text re.sub(r[^], , text) # 2. 去除方括号内容如[表情] text re.sub(r\[.*?\], , text) # 3. 全角转半角统一空格类型 text unicodedata.normalize(NFKC, text) text text.replace(\u3000, ).replace(\xa0, ) # 4. 去emoji emoji_pattern re.compile( [\U0001F300-\U0001F64F\U0001F680-\U0001F6FF\u2600-\u26FF\u2700-\u27BF \U0001F900-\U0001F9FF\U0001FA00-\U0001FA6F\U0001FA70-\U0001FAFF \u2B00-\u2BFF\U00002702-\U000027B0], flagsre.UNICODE, ) text emoji_pattern.sub(, text) # 5. 合并多余空白 text re.sub(r\s, , text).strip() return text df[review_content_clean] df[review_content].apply(clean_text)每一步的顺序有讲究。我习惯先去掉HTML标签再全角转半角最后去emoji。原因是HTML标签里可能包含全角引号或空格先去了标签可以避免后面处理残留标签碎片而NFKC全角转半角的处理会对部分全角符号生效但不影响emoji所以两者顺序可以灵活但一定要在strip()之前完成所有替换否则空白处理会不彻底。清洗完以后强烈建议再抽样一次看看清理效果df[[review_content, review_content_clean]].sample(5, random_state3)清洗前后对照着看你才能确认正则没有误伤正常文本。比如有些用户会故意用“”表达情绪这种标点重复其实是有业务含义的不能一刀切删掉。3.4 格式统一时间、评分、脱敏时间字段是重灾区。同一个表里出现“2024-06-15 12:30:00”和“2024/6/15 18:20”很常见。处理思路是让pandas自己识别常见格式识别不了的强制转成NaTdf[review_time] pd.to_datetime(df[review_time], errorscoerce) df df.dropna(subset[review_time])errorscoerce是关键。没有这个参数只要有一个非法时间格式整个转换就会直接报错有了它解析失败的值会变成NaT我们再对NaT做删除或填充。评分字段先抽取数字字符再转数值最后过滤越界值df[rating] ( df[rating] .astype(str) .str.extract(r(\d(?:\.\d)?))[0] .astype(float) ) df df[(df[rating] 1) (df[rating] 5)]这里用str.extract提取数字把“4分”“评分4.5”这类文本统一变成数值4.0或4.5。之后还需要决定要不要把小数评分取整我倾向于保留原始小数因为后续如果做评分预测小数信息是有价值的如果只是做分类统计再单独分桶。用户ID脱敏按“前2后2中间打码”的方式处理def mask_user_id(uid): s str(uid) if len(s) 4: return *** return s[:2] * * (len(s) - 4) s[-2:] df[user_id_masked] df[user_id].apply(mask_user_id)is_direct_purchase字段的六种写法用映射表统一purchase_map { 是: 是, 1: 是, True: 是, true: 是, 否: 否, 0: 否, False: 否, false: 否, } df[is_direct_purchase] df[is_direct_purchase].astype(str).map(purchase_map)处理完以后再用df[is_direct_purchase].value_counts()验证如果只剩“是/否”两个值这一列就干净了。3.5 异常值与矛盾数据规则化检测评价数据里的异常值光靠统计阈值很难发现因为真正的异常往往是业务逻辑层面的矛盾。最常见的矛盾是用户打了5星好评但评论内容里全是“垃圾”“退货”“差劲”这样的词。这种数据如果不处理后续做情感分析时会严重干扰模型训练。我用一个简单的关键词规则来标记negative_words [垃圾, 差, 退货, 退款, 客服, 太慢, 不值, 生气, 失望] def check_contradiction(row): content str(row[review_content_clean]) hits [w for w in negative_words if w in content] if row[rating] 4 and hits: return |.join(hits) return df[contradiction_hits] df.apply(check_contradiction, axis1)检出矛盾数据后不急着删我先打上标记然后在报告里单独列出由业务方决定是确认异常还是人工复评。这也是清洗里很重要的一条原则清洗规则可以自动化但疑似异常的最终裁决权要让给业务。除了矛盾文本还有一类异常是评论长度极端离谱比如一大段重复的“哈哈哈哈”明显是刷评论。我在后面单独用灌水检测来处理。3.6 灌水与默认好评识别电商评价里有两类典型的“僵尸数据”一类是超长刷屏另一类是连续重复字符。连续重复字符的检测用正则(.)\1{4,}能匹配同一个字符连续出现5次及以上的情况比如“啊啊啊啊”“”。repeat_pattern re.compile(r(.)\1{4,}) def flag_spam(text): if not isinstance(text, str): return 0 if len(text) 200: return 1 # 超长 if repeat_pattern.search(text): return 1 # 连续重复 return 0 df[is_spam] df[review_content_clean].apply(flag_spam)注意连续重复标点“”虽然命中规则但在中文评价里往往只是强调情绪不一定是灌水。所以我把这类数据标记出来而不是直接删除交给业务判断。超长评论则要看业务场景有的平台确实存在用户认真写长评的情况不能一概而论。标记而不是武断删除这是清洗的安全性底线。4. 踩坑记录五个不细看发现不了的细节4.1 NaN并不一定是“空”很多系统导出的CSV里缺失值长着四种不同的脸真正的空单元格、空字符串“”、字符串NaN、以及不可见字符\xa0或\u3000。df.isna()只能识别第一种后面三种都会被当成正常文本。如果不信你可以跑一下这个测试import numpy as np s pd.Series([np.nan, nan, , ]) print(s.isna().tolist()) # [True, False, False, False] print(s.astype(str).str.strip()) # nan 和 仍然是字符串解决办法是清洗前统一把各种“空”都转成NaNdf df.replace({: np.nan, nan: np.nan, NaN: np.nan, None: np.nan}) df[review_content] df[review_content].astype(str).str.strip() df[review_content] df[review_content].replace(, np.nan)这个坑我踩过好几次不统一处理后面fillna和dropna全都失灵。4.2 drop_duplicates 的默认行为会漏掉近似重复drop_duplicates()默认只看整行完全重复而业务里常见的重复是“同一个人买同一个商品写了几乎一样的内容但因为评价ID不同整行并不重复”。所以我前面专门用了subset[user_id, product_id, review_content]来限定判定字段。如果你漏了这一步报告里重复率会显示0%感觉数据很干净实际上重复一大片。去重之前先想想业务上什么样的重复才算重复4.3 正则不写 re.S换行文本清洗会翻车HTML标签清理时如果评论内容里含换行符p和/p可能跨行普通正则模式.匹配不到换行符导致标签清理不彻底。解决方法是给re.sub加flagsre.Stext re.sub(r[^], , text, flagsre.S)这个细节看起来很微小但真实数据里含有换行符的评论比例相当高尤其是在移动端输入的长评价里。不给re.S清洗结果就会出现漏网的半个标签。4.4 评分字段是“4分”而不是4我见过很多入门项目把评分字段直接astype(int)然后程序报错才发现评分是文本。更隐蔽的情况是评分里混着“4.0分”“4分半”这类格式。所以评分清洗必须先提取数字再转类型顺序反了就会抛异常或者把整列变成object。前面用的str.extract(r(\d(?:\.\d)?))能同时兼容整数和小数是一个比较稳的写法。4.5 去重顺序影响结果先做文本清洗再去重和先去重再清洗结果完全不同。我建议的顺序是先去重再清洗文本最后填充缺失。理由很直接如果两条相似评价只是格式不同一条带HTML一条不带先清洗再生效会让它们更容易被判定为重复导致该保留的信息被误删。先去重能保住更多原始信息虽然这可能让重复检测漏掉一些“伪装成不同格式”的重复但在信息保全优先的原则下这个取舍是值得的。5. 从清洗过程到最终报告输出与验证5.1 清洗日志每步保留数据量清洗最怕的是“不知道洗掉了什么”。所以我在每个清洗步骤之后都记录当前数据量汇总成一条清洗流水账。log [] def record(step, df_before, df_after, detail): log.append({ 步骤: step, 操作前数量: len(df_before), 操作后数量: len(df_after), 减少/处理数量: len(df_before) - len(df_after), 说明: detail, })每执行一步清洗就调用一次record()把前后数据量记录下来。最后生成报告的时候这些日志就是最可靠的数据来源。不要靠记忆写报告一定要让代码自己统计。5.2 Markdown报告结构清洗报告的最终形式我推荐用Markdown或数据字典表格输出既方便存档也方便贴进团队文档。核心结构如下# 电商用户评价数据清洗报告 ## 1. 数据概况 - 原始数据量1200条 - 清洗后数据量xxxx条 - 涉及字段12个 ## 2. 问题统计 | 问题类型 | 数量 | 处理方式 | |---|---|---| | 完全重复 | xx | 删除 | | 近似重复 | xx | 按关键字段去重 | | 缺失内容 | xx | 填充默认文案 | | 缺失时间 | xx | 删除 | | 评分异常 | xx | 删除 | | 矛盾数据 | xx | 标记待人工复核 | | 疑似灌水 | xx | 标记待人工复核 | ## 3. 清洗规则说明 ... ## 4. 数据质量对比 | 指标 | 清洗前 | 清洗后 | |---|---|---| | 重复率 | x% | 0% | | 缺失率 | x% | x% | | 文本格式统一度 | x% | 100% |5.3 清洗前后对比汇总清洗前后对比是报告里最有说服力的部分。我用一个函数汇总关键指标summary pd.DataFrame({ 指标: [总行数, 重复率, 缺失率, 评分合法率, 时间格式统一率], 清洗前: [ len(df_raw), f{df_raw.duplicated(subset[user_id,product_id,review_content]).mean() * 100:.1f}%, f{df_raw[review_content].isna().mean() * 100:.1f}%, f{(df_raw[rating].astype(str).str.extract(r(\\d))[0].astype(float).between(1,5)).mean() * 100:.1f}%, 0%, ], 清洗后: [ len(df), 0%, f{df[content_missing].mean() * 100:.1f}%, 100%, 100%, ], }) print(summary.to_markdown(indexFalse))清洗后报告里的数据要经得起别人追问“你这个重复率怎么算的”“这个缺失率包不包括空字符串”。写报告的时候每一步的算法口径我都在附录里写清楚宁可啰嗦一点不要含糊。6. 做了几轮清洗之后我想提醒你的几件事这套流程完整跑过几遍之后我最深的体会是清洗报告的真正价值不在那几张统计表而在于每一个处理动作都可追溯、可解释。我见过不少数据工程师交付的清洗代码跑完以后问他“你删了多少条为什么删”答不上来。这种清洗结果业务方是不敢用的。所以从现在开始养成每步记录日志、用问题清单驱动清洗的习惯比学会任何单个函数都重要。另外一个实用的技巧是清洗脚本尽量写成函数式管道结构而不是一长串从上到下的赋值语句。把clean_text、clean_rating、clean_time都封装成独立函数主流程只用几行调用这样后续新增清洗规则只需要加一个函数不需要动主逻辑。我自己维护的清洗脚本已经积累了二十多个这样的清洗函数换一个项目直接复用一套效率翻倍。这个项目做完以后如果你还想继续深挖有两条路可以走一是把清理完的评价文本接进jieba分词和情感分析把“好评/差评”的文本情感倾向识别出来二是基于评分和文本长度做异常评价识别模型把人工审核的范围进一步缩小。但无论走哪条路前提都是一样的——先把数据洗干净把报告写清楚。数据这行地基不牢上面盖什么都是危楼。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →