做数据分析时,我们拿到的原始数据往往是 “脏的”—— 比如订单数据里有缺失的 “优惠券金额”、异常的 “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 类 “脏数据”:

  1. 缺失值:订单 202401002 的 “优惠券金额” 是 NaN(表示缺失);
  2. 异常值:订单 202401003 的 “商品价格” 为 0(不可能,属于异常),订单 202401005 的 “商品价格” 15999(远超美妆类平均价格,可能是录入错误);
  3. 重复值:订单 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=1False=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=0how='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%)

填充是最常用的方法,关键是 “按字段类型选填充值”:

  1. 数值型字段(如销量、价格):用均值(mean)、中位数(median)填充(中位数更抗异常值);
  2. 分类型字段(如商品类别、用户 ID):用众数(mode,出现次数最多的值)填充;
  3. 时间型字段(如支付时间):用前向填充(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σ 原则 / 箱线图法则)—— 适合数值型字段
  1. 原则:数值落在均值 ±3 倍标准差之外,视为异常值(适合正态分布数据);
  2. 箱线图法则:数值落在 “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()

# 运行后会看到:015999两个点在箱线图外,是异常值

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行(删了订单202401003202401005

策略 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→5915999→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. 填充缺失值时数据类型不匹配

  1. 问题:用字符串填充数值型字段(比如用 “未填写” 填充销量),导致后续无法计算;
  2. 解决:填充前先查看字段类型(df.dtypes),数值型用数值填充(均值、0),分类型用字符串填充(众数、“未知”)。

2. 删除缺失值时误删重要数据

  1. 问题:用dropna()时没指定subset,导致 “优惠券金额” 缺失的行被误删;
  2. 解决:始终用subset指定 “必须无缺失的字段”(比如subset=["销量"]),只删除关键字段缺失的行。

3. 异常值判断脱离业务逻辑

  1. 问题:把 “1000 元的家电” 判定为异常值(实际是高端家电);
  2. 解决:异常值识别后,结合业务场景验证(比如家电类价格范围 100-10000 元),不要只看统计指标。

4. 重复值判断字段选择错误

  1. 问题:按 “商品类别” 判断重复,导致不同订单的同类商品被误删;
  2. 解决:按 “业务唯一标识” 判断重复(如订单 ID、用户 ID + 订单时间),避免用非唯一字段。

八、总结与后续学习方向

数据清洗不是 “机械删除 / 填充”,而是 “结合业务逻辑的筛选与修正”—— 核心是 “先理解数据含义,再选择合适方法”。今天我们掌握了:

  1. 缺失值处理:识别用isnull().sum(),处理用删除(dropna())或填充(fillna());
  2. 异常值处理:识别用箱线图 / 3σ 原则,处理用删除、修正或盖帽法;
  3. 重复值处理:识别用duplicated(),处理用drop_duplicates()
  4. 实战流程:从原始数据到干净数据的完整链路,以及保存结果的方法。

清洗后的 “干净数据” 可以用于后续分析:

  1. 基础统计:计算平均客单价、各品类销量占比;
  2. 用户分析:分析不同用户的消费频次、优惠券使用情况;
  3. 可视化:用 matplotlib 画 “品类销量柱状图”“价格分布直方图”。

下一篇我们会讲 “pandas 数据筛选与分组统计”,教你从清洗后的电商数据中提取有价值的信息(比如 “家电类高消费用户有哪些”“1 月各天的销量趋势”)。如果在清洗过程中遇到具体问题,欢迎留言分享你的数据场景,我会帮你针对性解决!

Logo

电商企业物流数字化转型必备!快递鸟 API 接口,72 小时快速完成物流系统集成。全流程实战1V1指导,营造开放的API技术生态圈。

更多推荐