Python 0基础练习 数据分析 3:pandas 数据清洗(缺失值 / 异常值 / 重复值处理,实战电商数据)
本文介绍了电商数据分析中的数据清洗方法,重点解决缺失值、异常值和重复值三大问题。通过Python的pandas库,作者演示了如何识别和处理脏数据:缺失值可通过删除或填充(均值/中位数/众数)处理;异常值可通过箱线图法则识别,并采用删除、修正或盖帽法处理;重复值则使用drop_duplicates()删除。文章提供了完整的电商订单数据清洗流程代码,并提醒注意常见错误如数据类型不匹配、脱离业务逻辑判断
做数据分析时,我们拿到的原始数据往往是 “脏的”—— 比如订单数据里有缺失的 “优惠券金额”、异常的 “0 元商品价格”、重复录入的 “订单 ID”。这些 “脏数据” 会直接导致分析结果失真(比如用含 0 元价格的数据算平均客单价,结果肯定不准)。
而 “数据清洗” 就是数据分析的 “过滤环节”,核心目标是 “剔除无效数据、修正异常数据、补充缺失数据”。今天这篇博客,会用电商订单数据做实战,手把手教你用 pandas 处理 “缺失值、异常值、重复值” 三大问题,所有代码可直接复制运行,新手也能轻松上手。
一、先明确:为什么要做数据清洗?(用电商案例说明)
假设我们有一份电商平台的 “2024 年 1 月订单数据”,原始数据包含以下字段:
|
订单 ID |
用户 ID |
商品类别 |
商品价格(元) |
销量 |
优惠券金额(元) |
支付时间 |
|
202401001 |
U001 |
家电 |
2999 |
1 |
100 |
2024-01-01 09:23 |
|
202401002 |
U002 |
服装 |
199 |
2 |
NaN |
2024-01-01 10:15 |
|
202401003 |
U003 |
食品 |
0 |
5 |
5 |
2024-01-01 11:08 |
|
202401004 |
U001 |
家电 |
2999 |
1 |
100 |
2024-01-01 09:23 |
|
202401005 |
U004 |
美妆 |
15999 |
1 |
500 |
2024-01-01 14:30 |
这份数据里藏着 3 类 “脏数据”:
- 缺失值:订单 202401002 的 “优惠券金额” 是 NaN(表示缺失);
- 异常值:订单 202401003 的 “商品价格” 为 0(不可能,属于异常),订单 202401005 的 “商品价格” 15999(远超美妆类平均价格,可能是录入错误);
- 重复值:订单 202401004 和 202401001 完全一样(属于重复录入)。
如果不清洗这些数据,直接计算 “平均商品价格”:
原始数据平均价格 = (2999+199+0+2999+15999)/5 = 4439.2 元,明显被 15999 和 0 拉高 / 拉低,清洗后才能得到真实的平均价格。
二、环境准备:快速上手(老读者可跳过)
如果还没安装 pandas,先执行以下命令(终端 / CMD 中):
|
pip install pandas openpyxl # openpyxl用于读取Excel数据 |
导入 pandas 并构造实战用的电商订单数据(不用自己找数据,直接复制代码生成):
|
import pandas as pd import numpy as np # 用于生成缺失值和异常值 # 构造电商订单原始数据(含缺失值、异常值、重复值) order_data = { "订单ID": ["202401001", "202401002", "202401003", "202401004", "202401005", "202401006", "202401007"], "用户ID": ["U001", "U002", "U003", "U001", "U004", "U005", "U003"], "商品类别": ["家电", "服装", "食品", "家电", "美妆", "服装", "食品"], "商品价格(元)": [2999, 199, 0, 2999, 15999, 299, 59], "销量": [1, 2, 5, 1, 1, 3, np.nan], # np.nan表示缺失值 "优惠券金额(元)": [100, np.nan, 5, 100, 500, 30, 0], # 缺失值 "支付时间": ["2024-01-01 09:23", "2024-01-01 10:15", "2024-01-01 11:08", "2024-01-01 09:23", "2024-01-01 14:30", "2024-01-01 16:45", "2024-01-01 18:20"] } # 转为DataFrame(后续所有操作基于此) df = pd.DataFrame(order_data) print("原始电商订单数据:") print(df) |
运行后会看到包含 7 条订单的原始数据,接下来我们针对 “缺失值、异常值、重复值” 逐一清洗。
三、缺失值处理:识别→选择合适的处理策略
缺失值(NaN)是最常见的 “脏数据”,比如用户没使用优惠券时 “优惠券金额” 为空、系统故障导致 “销量” 未记录。处理核心是 “先识别缺失情况,再根据字段类型选处理方法”。
1. 第一步:识别缺失值(3 个核心方法)
先搞清楚 “哪些字段有缺失、缺失了多少”,避免盲目处理。
方法 1:查看每列缺失值数量(isnull ().sum ())
|
# 计算每列缺失值数量(True=1,False=0,求和即缺失数) missing_count = df.isnull().sum() print("\n每列缺失值数量:") print(missing_count) # 输出: # 订单ID 0 # 用户ID 0 # 商品类别 0 # 商品价格(元) 0 # 销量 1 # 销量列有1个缺失值 # 优惠券金额(元) 1 # 优惠券金额列有1个缺失值 # 支付时间 0 # dtype: int64 |
方法 2:查看缺失值占比(更直观)
|
# 计算缺失值占比(缺失数/总行数 * 100,保留2位小数) missing_ratio = (df.isnull().sum() / len(df)) * 100 print("\n每列缺失值占比(%):") print(missing_ratio.round(2)) # 输出: # 销量 14.29 # 缺失占比14.29% # 优惠券金额(元) 14.29 # 缺失占比14.29% |
方法 3:查看缺失值所在的行(定位具体数据)
|
# 查看包含缺失值的所有行(只显示前5列,避免输出过长) missing_rows = df[df.isnull().any(axis=1)] # axis=1:按行判断是否有缺失 print("\n包含缺失值的行:") print(missing_rows.iloc[:, :5]) # iloc[:, :5]:取所有行,前5列 # 输出: # 订单ID 用户ID 商品类别 商品价格(元) 销量 # 5 202401006 U005 服装 299 NaN # 销量缺失 # 1 202401002 U002 服装 199 2.0 # 优惠券金额缺失 |
2. 第二步:处理缺失值(4 种实用策略,按场景选)
根据字段类型(数值型 / 分类型)和缺失占比,选择不同的处理方法:
策略 1:删除缺失值(dropna ())—— 适合缺失占比低(<5%)且字段重要性低
如果缺失值很少(比如 1%),直接删除含缺失值的行 / 列,对整体数据影响小。
|
# 方法1:删除所有含缺失值的行(默认axis=0,how='any':只要有一个缺失就删) df_drop_any = df.dropna(axis=0, how='any') print(f"\n删除所有含缺失值的行后,行数:{len(df_drop_any)}(原始:{len(df)})") # 输出:删除后行数:5(原始7行,删了2行含缺失的) # 方法2:只删除“所有字段都缺失”的行(how='all',极少用) df_drop_all = df.dropna(axis=0, how='all') print(f"只删除全缺失的行后,行数:{len(df_drop_all)}(原始:{len(df)})") # 输出:行数不变(没有全缺失的行) # 方法3:指定字段缺失时才删除(subset参数,推荐!避免误删其他字段) # 比如:只有“销量”缺失时才删除,“优惠券金额”缺失不删 df_drop_subset = df.dropna(axis=0, subset=["销量"]) print(f"只删除销量缺失的行后,行数:{len(df_drop_subset)}(原始:{len(df)})") # 输出:行数:6(只删了订单202401006) |
策略 2:填充缺失值(fillna ())—— 适合缺失占比中等(5%-30%)
填充是最常用的方法,关键是 “按字段类型选填充值”:
- 数值型字段(如销量、价格):用均值(mean)、中位数(median)填充(中位数更抗异常值);
- 分类型字段(如商品类别、用户 ID):用众数(mode,出现次数最多的值)填充;
- 时间型字段(如支付时间):用前向填充(ffill,取上一行的值)或后向填充(bfill,取下一行的值)。
|
# 先复制一份数据,避免修改原始数据 df_fill = df.copy() # 1. 数值型字段填充:销量(用中位数填充) sales_median = df_fill["销量"].median() # 计算销量的中位数 df_fill["销量"] = df_fill["销量"].fillna(sales_median) print(f"\n销量缺失值用中位数({sales_median})填充后:") print(df_fill[["订单ID", "销量"]].iloc[[5]]) # 查看之前缺失的行(索引5) # 输出:订单202401006的销量从NaN变成1.0(中位数) # 2. 数值型字段填充:优惠券金额(用0填充,逻辑:缺失=没使用优惠券) # (优惠券金额缺失更符合“没使用”,用0比均值更合理,需结合业务逻辑!) df_fill["优惠券金额(元)"] = df_fill["优惠券金额(元)"].fillna(0) print(f"\n优惠券金额缺失值用0填充后:") print(df_fill[["订单ID", "优惠券金额(元)"]].iloc[[1]]) # 查看之前缺失的行(索引1) # 输出:订单202401002的优惠券金额从NaN变成0 # 3. 分类型字段填充示例(假设“商品类别”有缺失,用众数填充) # 先模拟一个商品类别缺失值 df_fill_test = df_fill.copy() df_fill_test.loc[6, "商品类别"] = np.nan # 把索引6的商品类别设为缺失 category_mode = df_fill_test["商品类别"].mode()[0] # 众数(取第一个众数) df_fill_test["商品类别"] = df_fill_test["商品类别"].fillna(category_mode) print(f"\n商品类别缺失值用众数({category_mode})填充后:") print(df_fill_test[["订单ID", "商品类别"]].iloc[[6]]) # 输出:商品类别从NaN变成“家电”(假设众数是家电) |
策略 3:用模型预测填充(进阶)—— 适合缺失占比高(>30%)
如果缺失占比高(比如 50%),删除或简单填充会损失太多信息,可通过机器学习模型(如线性回归、随机森林)根据其他字段预测缺失值(新手先掌握前两种,后续再学进阶)。
策略 4:保留缺失值(特殊场景)
如果缺失值本身有业务意义(比如 “优惠券金额缺失 = 用户未登录”),可保留缺失值,后续分析时单独标记为 “未登录用户”,不用强行处理。
四、异常值处理:识别→修正 / 剔除(避免干扰分析)
异常值(比如商品价格 0 元、销量 1000 件)会严重干扰统计结果(如平均价格、总销量),处理核心是 “先通过可视化 / 统计方法识别,再根据异常原因修正或剔除”。
1. 第一步:识别异常值(2 个常用方法)
方法 1:统计法(3σ 原则 / 箱线图法则)—— 适合数值型字段
- 3σ 原则:数值落在均值 ±3 倍标准差之外,视为异常值(适合正态分布数据);
- 箱线图法则:数值落在 “Q1-1.5IQR” 或 “Q3+1.5IQR” 之外(Q1 = 下四分位,Q3 = 上四分位,IQR=Q3-Q1),视为异常值(更通用,抗极端值)。
|
# 用箱线图法则识别“商品价格(元)”的异常值 # 1. 计算四分位数和IQR q1 = df_fill["商品价格(元)"].quantile(0.25) # 下四分位(25%分位数) q3 = df_fill["商品价格(元)"].quantile(0.75) # 上四分位(75%分位数) iqr = q3 - q1 # 四分位距 # 2. 计算异常值的上下边界 lower_bound = q1 - 1.5 * iqr # 下边界 upper_bound = q3 + 1.5 * iqr # 上边界 print(f"\n商品价格异常值识别(箱线图法则):") print(f"下四分位(Q1):{q1},上四分位(Q3):{q3},IQR:{iqr}") print(f"异常值下边界:{lower_bound},上边界:{upper_bound}") # 3. 找出异常值 price_outliers = df_fill[(df_fill["商品价格(元)"] < lower_bound) | (df_fill["商品价格(元)"] > upper_bound)] print(f"\n商品价格异常的订单:") print(price_outliers[["订单ID", "商品类别", "商品价格(元)"]]) # 输出:订单202401003(价格0)、202401005(价格15999)是异常值 |
方法 2:可视化法(箱线图 / 直方图)—— 直观看到异常值
用 matplotlib 画箱线图,能直观看到异常值(箱线图外的点):
|
import matplotlib.pyplot as plt plt.rcParams['font.sans-serif'] = ['SimHei'] # 解决中文显示问题 # 画商品价格的箱线图 plt.figure(figsize=(8, 4)) plt.boxplot(df_fill["商品价格(元)"], labels=["商品价格"]) plt.title("电商订单商品价格箱线图(红圈为异常值)") plt.ylabel("价格(元)") plt.grid(linestyle="--", alpha=0.5) plt.show() # 运行后会看到:0和15999两个点在箱线图外,是异常值 |
2. 第二步:处理异常值(3 种策略,结合业务逻辑)
处理异常值不能盲目删除,要先分析异常原因(录入错误?特殊情况?):
策略 1:删除异常值(适合确认是错误数据)
如果异常值是 “录入错误”(比如价格 15999 应该是 1599,0 元是未录入),且数量少,直接删除:
|
# 复制填充后的数据,继续处理异常值 df_outlier = df_fill.copy() # 删除商品价格异常的行(价格<lower_bound 或 >upper_bound) df_outlier_clean = df_outlier[ (df_outlier["商品价格(元)"] >= lower_bound) & (df_outlier["商品价格(元)"] <= upper_bound) ] print(f"\n删除商品价格异常值后:") print(f"原始行数:{len(df_outlier)},清洗后行数:{len(df_outlier_clean)}") # 输出:原始7行,清洗后5行(删了订单202401003和202401005) |
策略 2:修正异常值(适合可追溯的错误)
如果能通过业务逻辑修正(比如价格 15999 是多写了一个 9,应该是 1599),直接修正:
|
# 修正异常值:假设价格15999是录入错误,应为1599;价格0应为59(食品类平均价格) df_outlier_fix = df_outlier.copy() # 修正价格15999→1599 df_outlier_fix.loc[df_outlier_fix["商品价格(元)"] == 15999, "商品价格(元)"] = 1599 # 修正价格0→59(食品类的平均价格,可计算实际均值) food_avg_price = df_outlier_fix[df_outlier_fix["商品类别"] == "食品"]["商品价格(元)"].mean() df_outlier_fix.loc[df_outlier_fix["商品价格(元)"] == 0, "商品价格(元)"] = food_avg_price print(f"\n修正异常值后的商品价格:") print(df_outlier_fix[["订单ID", "商品类别", "商品价格(元)"]].iloc[[2, 4]]) # 输出:订单202401003价格从0→59,订单202401005价格从15999→1599 |
策略 3:盖帽法(适合异常值是极端值,不想删除)
如果异常值是 “真实存在的极端值”(比如确实有用户买了高价商品),用 “99 分位数” 或 “1 分位数” 替换,避免影响整体统计:
|
# 用99分位数和1分位数做盖帽处理 df_outlier_cap = df_outlier.copy() # 计算1分位数和99分位数 p1 = df_outlier_cap["商品价格(元)"].quantile(0.01) p99 = df_outlier_cap["商品价格(元)"].quantile(0.99) # 低于p1的设为p1,高于p99的设为p99 df_outlier_cap["商品价格(元)"] = df_outlier_cap["商品价格(元)"].apply( lambda x: p1 if x < p1 else (p99 if x > p99 else x) ) print(f"\n盖帽法处理后,商品价格范围:{df_outlier_cap['商品价格(元)'].min()} ~ {df_outlier_cap['商品价格(元)'].max()}") # 输出:价格范围更合理(比如0→59,15999→15999(若p99≥15999则不变)) |
五、重复值处理:识别→删除(避免重复统计)
重复值(比如同一订单重复录入两次)会导致 “总销量、总订单数” 统计偏高,处理逻辑最简单:“识别重复行→保留一行,删除其余”。
1. 第一步:识别重复值(duplicated ())
|
# 基于当前清洗后的数据(df_outlier_fix),识别重复值 # 方法1:查看所有重复行(默认keep='first':第一次出现的视为非重复,后续为重复) duplicated_rows = df_outlier_fix[df_outlier_fix.duplicated(keep='first')] print(f"\n重复的订单行:") print(duplicated_rows) # 输出:订单202401004(和202401001完全重复) # 方法2:查看每列重复值数量(适合判断单个字段重复,比如订单ID重复) order_dup_count = df_outlier_fix["订单ID"].duplicated().sum() print(f"\n订单ID重复的数量:{order_dup_count}") # 输出:1(订单ID 202401004重复) # 方法3:判断是否基于指定字段重复(比如“订单ID”唯一,只要订单ID重复就是重复行) dup_by_order = df_outlier_fix.duplicated(subset=["订单ID"], keep='first') print(f"\n基于订单ID的重复行索引:{df_outlier_fix[dup_by_order].index.tolist()}") # 输出:[3](索引3的行是重复行) |
2. 第二步:删除重复值(drop_duplicates ())
|
# 复制数据,处理重复值 df_dup_clean = df_outlier_fix.copy() # 方法1:删除所有完全重复的行(默认keep='first',保留第一次出现的) df_dup_clean1 = df_dup_clean.drop_duplicates(keep='first') print(f"\n删除完全重复行后,行数:{len(df_dup_clean1)}(原始:{len(df_dup_clean)})") # 输出:原始7行→6行(删了重复的订单202401004) # 方法2:基于指定字段删除重复行(推荐!按业务唯一标识删除,如订单ID) df_dup_clean2 = df_dup_clean.drop_duplicates(subset=["订单ID"], keep='first') print(f"基于订单ID删除重复行后,行数:{len(df_dup_clean2)}(原始:{len(df_dup_clean)})") # 输出:同样6行(效果和方法1一致,更符合业务逻辑) # 方法3:保留最后一次出现的重复行(keep='last') df_dup_clean3 = df_dup_clean.drop_duplicates(subset=["订单ID"], keep='last') print(f"保留最后一次重复行的订单ID:{df_dup_clean3['订单ID'].tolist()}") # 输出:订单ID包含202401004,不包含202401001(保留最后一次) |
六、实战:电商订单数据完整清洗流程(整合所有步骤)
把前面的缺失值、异常值、重复值处理整合,形成完整的清洗流程,从 “原始数据” 到 “干净数据”:
|
# 完整清洗流程代码 import pandas as pd import numpy as np # 1. 加载原始数据(或构造数据) order_data = { "订单ID": ["202401001", "202401002", "202401003", "202401004", "202401005", "202401006", "202401007"], "用户ID": ["U001", "U002", "U003", "U001", "U004", "U005", "U003"], "商品类别": ["家电", "服装", "食品", "家电", "美妆", "服装", "食品"], "商品价格(元)": [2999, 199, 0, 2999, 15999, 299, 59], "销量": [1, 2, 5, 1, 1, np.nan, 3], "优惠券金额(元)": [100, np.nan, 5, 100, 500, 30, 0], "支付时间": ["2024-01-01 09:23", "2024-01-01 10:15", "2024-01-01 11:08", "2024-01-01 09:23", "2024-01-01 14:30", "2024-01-01 16:45", "2024-01-01 18:20"] } df = pd.DataFrame(order_data) print("=== 原始数据 ===") print(df) # 2. 缺失值处理 df_clean = df.copy() # 销量用中位数填充 df_clean["销量"] = df_clean["销量"].fillna(df_clean["销量"].median()) # 优惠券金额用0填充(业务逻辑:缺失=未使用) df_clean["优惠券金额(元)"] = df_clean["优惠券金额(元)"].fillna(0) print("\n=== 缺失值处理后 ===") print(df_clean[["订单ID", "销量", "优惠券金额(元)"]]) # 3. 异常值处理(修正商品价格) # 修正价格0→食品类均值,15999→1599 food_avg = df_clean[df_clean["商品类别"] == "食品"]["商品价格(元)"].mean() df_clean.loc[df_clean["商品价格(元)"] == 0, "商品价格(元)"] = food_avg df_clean.loc[df_clean["商品价格(元)"] == 15999, "商品价格(元)"] = 1599 print("\n=== 异常值处理后 ===") print(df_clean[["订单ID", "商品价格(元)"]]) # 4. 重复值处理(按订单ID删除重复) df_clean = df_clean.drop_duplicates(subset=["订单ID"], keep='first') print("\n=== 重复值处理后(最终干净数据) ===") print(df_clean) # 5. 保存清洗后的数据(可选,用于后续分析) df_clean.to_excel("清洗后的电商订单数据.xlsx", index=False, engine="openpyxl") print(f"\n=== 清洗完成 ===") print(f"原始数据行数:{len(df)},清洗后行数:{len(df_clean)}") print(f"清洗后数据已保存为:清洗后的电商订单数据.xlsx") |
清洗效果对比:
|
指标 |
原始数据 |
清洗后数据 |
变化原因 |
|
总行数 |
7 |
6 |
删除 1 条重复行(订单 202401004) |
|
缺失值数量 |
2 |
0 |
销量用中位数填充,优惠券金额用 0 填充 |
|
商品价格异常值 |
2 |
0 |
0 元修正为 59,15999 修正为 1599 |
|
订单 ID 重复数 |
1 |
0 |
删除重复的订单 ID |
七、新手常见坑点及解决办法
1. 填充缺失值时数据类型不匹配
- 问题:用字符串填充数值型字段(比如用 “未填写” 填充销量),导致后续无法计算;
- 解决:填充前先查看字段类型(df.dtypes),数值型用数值填充(均值、0),分类型用字符串填充(众数、“未知”)。
2. 删除缺失值时误删重要数据
- 问题:用dropna()时没指定subset,导致 “优惠券金额” 缺失的行被误删;
- 解决:始终用subset指定 “必须无缺失的字段”(比如subset=["销量"]),只删除关键字段缺失的行。
3. 异常值判断脱离业务逻辑
- 问题:把 “1000 元的家电” 判定为异常值(实际是高端家电);
- 解决:异常值识别后,结合业务场景验证(比如家电类价格范围 100-10000 元),不要只看统计指标。
4. 重复值判断字段选择错误
- 问题:按 “商品类别” 判断重复,导致不同订单的同类商品被误删;
- 解决:按 “业务唯一标识” 判断重复(如订单 ID、用户 ID + 订单时间),避免用非唯一字段。
八、总结与后续学习方向
数据清洗不是 “机械删除 / 填充”,而是 “结合业务逻辑的筛选与修正”—— 核心是 “先理解数据含义,再选择合适方法”。今天我们掌握了:
- 缺失值处理:识别用isnull().sum(),处理用删除(dropna())或填充(fillna());
- 异常值处理:识别用箱线图 / 3σ 原则,处理用删除、修正或盖帽法;
- 重复值处理:识别用duplicated(),处理用drop_duplicates();
- 实战流程:从原始数据到干净数据的完整链路,以及保存结果的方法。
清洗后的 “干净数据” 可以用于后续分析:
- 基础统计:计算平均客单价、各品类销量占比;
- 用户分析:分析不同用户的消费频次、优惠券使用情况;
- 可视化:用 matplotlib 画 “品类销量柱状图”“价格分布直方图”。
下一篇我们会讲 “pandas 数据筛选与分组统计”,教你从清洗后的电商数据中提取有价值的信息(比如 “家电类高消费用户有哪些”“1 月各天的销量趋势”)。如果在清洗过程中遇到具体问题,欢迎留言分享你的数据场景,我会帮你针对性解决!
更多推荐


所有评论(0)