点击获取AI摘要

Python 与数据清洗

1. 数据清洗的重要性

在机器学习项目中,数据清洗是至关重要的第一步。它指的是识别并修正数据集中的错误、不一致性、不准确性或缺失部分,以保证数据的质量和适用性。高质量的数据是构建可靠机器学习模型的基石。如果输入到模型中的数据存在缺陷,即使是最先进的算法也无法产生有意义的结果。这正如一句谚语所说:“垃圾进,垃圾出”。

不良的数据质量可能导致:

  • 模型训练不准确
  • 预测结果不可靠
  • 最终损害整个项目的价值与可用性

因此,投入时间和精力进行彻底的数据清洗,是项目成功的先决条件。

1.1 “脏数据”的常见来源

  • 人为错误
    • 拼写错误
    • 数值录入失误
  • 测量设备故障或精度限制
    • 传感器数据偏差
    • 仪器读数不稳定
  • 数据记录不完整
    • 用户注册时未填写某些字段
    • 采集过程丢失数据
  • 多源数据格式不一致
    • 日期格式(如 YYYY-MM-DD vs DD/MM/YYYY
    • 单位表示差异(如米 vs 英尺)

2. 处理缺失值

缺失值是指在数据集中某些观测的特定特征没有记录或无法获取的情况。理解缺失值的类型对于选择合适的处理方法至关重要。

2.1 缺失值的类型

  1. 完全随机缺失(MCAR,Missing Completely At Random)

    • 数据缺失的概率与其他任何观测或特征都无关。
    • 例如:实验室设备偶然故障导致部分测量结果丢失。
  2. 随机缺失(MAR,Missing At Random)

    • 数据缺失的概率可能与某些已观测到的特征有关,但与缺失值本身无关。
    • 例如:收入高的人倾向于不填写“家庭收入”字段,但缺失并不依赖于收入数值本身。
  3. 非随机缺失(MNAR,Missing Not At Random)

    • 数据缺失的概率与缺失值本身有关。
    • 例如:抑郁症患者可能因为症状严重而不愿意填写精神健康调查。

2.2 处理缺失值的主要方法

Tip: 选择何种方法,要综合考虑数据量、缺失比例及其分布机制。

2.2.1 删除(Deletion)

  1. 行删除

    • 删除包含任何缺失值的整行。
    • 优点:简单易行;
    • 缺点:可能丢失大量信息,尤其在缺失比例较高时容易引入偏差。
    • 适用场景:数据集很大且缺失值非常少时。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    import pandas as pd

    # 读取示例 DataFrame
    df = pd.DataFrame({
    'A': [1, 2, None, 4],
    'B': [5, None, 7, 8],
    'C': ['a', 'b', 'c', None]
    })

    # 删除任何含有缺失值的行
    df_drop_rows = df.dropna()
    print(df_drop_rows)
  2. 列删除

    • 删除缺失值比例超过某个阈值的列。
    • 优点:避免模型被缺失信息大规模干扰;
    • 缺点:可能丢失重要特征,需要谨慎判断。
    • 适用场景:某个特征缺失比例过高且无法合理填充时。
    1
    2
    3
    4
    # 假设阈值为 50%
    threshold = 0.5
    df_drop_cols = df.dropna(axis=1, thresh=int((1 - threshold) * len(df)))
    print(df_drop_cols)

2.2.2 填充(Imputation)

  1. 统计填充

    • 均值填充:用该特征的平均值替换缺失值。

      1
      2
      3
      df_mean = df.copy()
      df_mean['A'] = df_mean['A'].fillna(df_mean['A'].mean())
      print(df_mean)
    • 中位数填充:用该特征的中位数替换缺失值,对异常值更鲁棒。

      1
      2
      3
      df_median = df.copy()
      df_median['A'] = df_median['A'].fillna(df_median['A'].median())
      print(df_median)
    • 众数填充:常用于类别型特征,用最频繁出现的值替换缺失值。

      1
      2
      3
      4
      df_mode = df.copy()
      mode_value = df_mode['C'].mode()[0]
      df_mode['C'] = df_mode['C'].fillna(mode_value)
      print(df_mode)
  2. 基于模型的填充

    • 使用回归、KNN 等模型,根据其他特征预测缺失值。
    • 优点:能够捕捉各变量之间的关系,更准确;
    • 缺点:实现复杂、计算成本高。
    1
    2
    3
    4
    5
    from sklearn.impute import KNNImputer

    imputer = KNNImputer(n_neighbors=3)
    df_knn = pd.DataFrame(imputer.fit_transform(df[['A', 'B']]), columns=['A', 'B'])
    print(df_knn)
  3. 创建缺失值指示器(Missing Indicator)

    • 不直接填充缺失值,而是生成一个二元特征,标记原始位置是否缺失。
    • 优点:保留了“缺失”本身的信息,有时缺失就是一个信号;
    • 缺点:增加数据维度。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    df_indicator = df.copy()

    # 创建指示器列,1 表示原始值缺失,0 表示不缺失
    df_indicator['A_missing'] = df_indicator['A'].isnull().astype(int)
    df_indicator['B_missing'] = df_indicator['B'].isnull().astype(int)

    # 对缺失值进行填充(这里以均值为例)
    df_indicator['A'] = df_indicator['A'].fillna(df_indicator['A'].mean())
    df_indicator['B'] = df_indicator['B'].fillna(df_indicator['B'].mean())

    print(df_indicator)

2.3 小结

  • 没有“万能方法”,要根据数据缺失类型和业务场景选择合适策略。
  • 如果缺失不随机,简单的统计填充会扭曲数据分布;
  • 理解缺失背后的原因,才能做出明智的处理决定。

表 1: 处理缺失值的常用技术

技术 描述 优点 缺点 典型应用场景
行删除 删除包含任何缺失值的行 简单易行 可能丢失宝贵信息,引入偏差 数据集很大且缺失值很少
列删除 删除包含大量缺失值的列 可以避免模型被缺失值干扰 可能丢失重要特征 某特征缺失值过多,难以合理填充
均值填充 使用特征的平均值填充缺失值 简单快速 降低方差,对异常值敏感 缺失值较少,数据分布近似正态
中位数填充 使用特征的中位数填充缺失值 对异常值不敏感 可能改变原始数据分布 数据存在较多异常值
众数填充 使用特征的众数填充缺失值 适用于类别型特征 可能引入偏差 类别型特征存在缺失值
基于模型的填充 使用统计模型预测缺失值 可以捕捉变量间关系,填充更准确 实现复杂,计算成本高 缺失值模式复杂,需要更精确的填充
缺失值指示器 创建一个二元特征标记缺失情况 保留缺失信息 增加数据维度 缺失本身可能包含有用信息

3. 处理异常值

异常值指的是与其他观测值显著不同的数据点。它们可能源于测量误差、录入错误,也可能是真实世界中罕见事件的反映。异常值会对模型产生扰动,扭曲分布、降低性能,甚至导致错误结论。

3.1 检测异常值的方法

3.1.1 可视化方法

  1. 箱线图(Box Plot)

    • 显示数据的四分位数(Q1、Q2、Q3)和四分位距(IQR)。
    • 将超出 Q1 - 1.5·IQRQ3 + 1.5·IQR 的点视为潜在异常值。
    1
    2
    3
    4
    5
    import matplotlib.pyplot as plt

    plt.boxplot(df['A'].dropna())
    plt.title("Boxplot of A")
    plt.show()
  2. 散点图(Scatter Plot)

    • 适用于双变量数据,直观发现与其他点显著分离的观测。
    1
    2
    3
    4
    5
    plt.scatter(df['A'], df['B'])
    plt.xlabel("A")
    plt.ylabel("B")
    plt.title("Scatter Plot of A vs B")
    plt.show()
  3. 直方图(Histogram)

    • 显示数据分布,可观察尾部是否有孤立的条柱。
    1
    2
    3
    4
    5
    plt.hist(df['A'].dropna(), bins=10)
    plt.xlabel("A")
    plt.ylabel("Frequency")
    plt.title("Histogram of A")
    plt.show()

3.1.2 统计方法

  1. Z-分数(Z-Score)

    • 计算每个数据点与均值的距离,以标准差为单位。
    • 通常 |Z| > 3 的值被认为是异常。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    import numpy as np

    mean_A = df['A'].mean()
    std_A = df['A'].std()
    df['A_zscore'] = (df['A'] - mean_A) / std_A

    # 标记异常值
    outliers_z = df[np.abs(df['A_zscore']) > 3]
    print(outliers_z)
  2. IQR 方法

    • 基于箱线图思想,将落在 Q1 - 1.5·IQRQ3 + 1.5·IQR 之外的值视为异常。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    Q1 = df['A'].quantile(0.25)
    Q3 = df['A'].quantile(0.75)
    IQR = Q3 - Q1

    lower_bound = Q1 - 1.5 * IQR
    upper_bound = Q3 + 1.5 * IQR

    outliers_iqr = df[(df['A'] < lower_bound) | (df['A'] > upper_bound)]
    print(outliers_iqr)
  3. Isolation Forest(孤立森林)

    • 基于树的集成方法,通过不断随机切分特征空间来“隔离”异常值。
    • 适用于高维数据,检测复杂模式的异常。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    from sklearn.ensemble import IsolationForest

    iso = IsolationForest(contamination=0.1, random_state=42)
    df_nonull = df[['A', 'B']].dropna()
    iso.fit(df_nonull)

    df_nonull['anomaly_score'] = iso.decision_function(df_nonull)
    df_nonull['anomaly_flag'] = iso.predict(df_nonull) # -1 为异常,1 为正常

    print(df_nonull[df_nonull['anomaly_flag'] == -1])

3.2 处理异常值的策略

  1. 删除

    • 直接移除被识别的异常点。
    • 适用于确认是数据输入错误或设备误差时,需谨慎避免丢失真实但极端的观测。
    1
    df_clean = df[~((df['A'] < lower_bound) | (df['A'] > upper_bound))]
  2. 转换

    • 使用 对数转换截尾(Winsorizing) 等方法,减小极端值影响。
    • 例如,对数转换:A_log = np.log(df['A']),适用于正值且偏态分布的数据。
    1
    df['A_log'] = np.log(df['A'].replace(0, np.nan).dropna())
  3. 填充

    • 用更合理的边界值或均值、中位数替换异常值。
    1
    df['A_clipped'] = df['A'].clip(lower=lower_bound, upper=upper_bound)
  4. 保留

    • 不做处理,将异常值视为潜在信号。
    • 常见于 异常检测欺诈识别 等场景。

表 2: 检测和处理异常值的常用技术

技术 描述 优点 缺点 典型应用场景
箱线图 可视化数据的分布和潜在异常值 直观易懂 对高维数据不适用 初步探索性数据分析
Z-分数 衡量数据点距离均值的标准差个数 易于计算 对数据分布有要求,对异常值敏感 数据分布近似正态
IQR方法 基于四分位数间距识别异常值 对异常值不敏感 可能遗漏极端异常值 数据存在异常值
孤立森林 基于树的集成方法,通过隔离异常值进行检测 适用于高维数据,性能较好 参数调整可能复杂 复杂数据集的异常值检测
删除 移除被识别为异常值的数据点 简单直接 可能丢失信息 明显的错误数据或噪声
转换 使用数学函数(如对数)调整数据分布 降低极端值的影响 可能改变数据的原始含义 数据偏态分布
填充 用其他值(如均值、中位数或边界值)替换异常值 保留数据量 可能引入偏差 异常值数量不多,且有合理的替换值
保留 不对异常值进行处理 避免丢失潜在重要信息 异常值可能影响模型性能 异常值可能代表重要事件或模式

4. 数据转换技术

数据转换是将原始数据转换为更适合模型处理的格式或分布。合理的转换能够提升模型性能与稳定性。

4.1 缩放与归一化

  1. 最小–最大缩放(Min–Max Scaling)

    • 将数值线性映射到 [0, 1] 区间:

      xscaled=xxminxmaxxminx_{\text{scaled}} = \frac{x - x_{\text{min}}}{x_{\text{max}} - x_{\text{min}}}

    • 优点:直观;

    • 缺点:对异常值敏感。

    1
    2
    3
    4
    5
    from sklearn.preprocessing import MinMaxScaler

    scaler = MinMaxScaler()
    df_scaled = scaler.fit_transform(df[['A', 'B']].dropna())
    print(df_scaled[:5])
  2. 标准化(Standardization,Z-Score Scaling)

    • 转换为均值为 0、标准差为 1 的分布:

      xstandardized=xμσx_{\text{standardized}} = \frac{x - \mu}{\sigma}

    • 对异常值相对鲁棒一些,但对极端值仍有影响。

    1
    2
    3
    4
    5
    from sklearn.preprocessing import StandardScaler

    std_scaler = StandardScaler()
    df_standard = std_scaler.fit_transform(df[['A', 'B']].dropna())
    print(df_standard[:5])
  3. 鲁棒缩放(Robust Scaling)

    • 基于中位数和四分位数进行缩放,对异常值更不敏感。
    1
    2
    3
    4
    5
    from sklearn.preprocessing import RobustScaler

    robust_scaler = RobustScaler()
    df_robust = robust_scaler.fit_transform(df[['A', 'B']].dropna())
    print(df_robust[:5])**鲁棒缩放**是一种对异常值不太敏感的缩放方法,例如scikit-learn中的`RobustScaler`,它基于数据的百分位数进行缩放。

4.2 非线性转换

  1. 对数转换(Log Transformation)

    • 用于处理偏态分布,将大值压缩。
    • 仅适用于正值数据。
    1
    df['A_log'] = np.log(df['A'].replace(0, np.nan)).dropna()
  2. 幂转换(Power Transformation,如 Box–Cox、Yeo–Johnson)

    • 更通用的方法,常用于稳定方差、使数据更接近正态分布。
    1
    2
    3
    4
    5
    from sklearn.preprocessing import PowerTransformer

    pt = PowerTransformer(method='yeo-johnson')
    df_power = pt.fit_transform(df[['A', 'B']].dropna())
    print(df_power[:5])

4.3 类别型变量编码

  1. 独热编码(One-Hot Encoding)

    • 对于机器学习模型,类别型变量通常需要转换为数值形式。独热编码是一种常用的方法,它为每个类别创建一个新的二元特征,如果某个观测属于该类别,则该特征的值为1,否则为0。
    • 为每个类别生成一个二元特征。
    • 适用于无序类别变量,但会增加维度。
    1
    2
    3
    df_cat = pd.DataFrame({'color': ['red', 'blue', 'green', 'blue', None]})
    df_cat_onehot = pd.get_dummies(df_cat['color'], dummy_na=True)
    print(df_cat_onehot)
  2. 标签编码(Label Encoding)

    • 标签编码将每个类别分配一个唯一的整数。
    • 将类别变量映射为整数。
    • 适用于有序类别,可能会引入无意义的顺序关系。
    1
    2
    3
    4
    5
    from sklearn.preprocessing import LabelEncoder

    le = LabelEncoder()
    df_cat['color_encoded'] = le.fit_transform(df_cat['color'].astype(str))
    print(df_cat)

4.4 选择合适的转换方法

  • 基于距离的算法(如 KNN、K-Means):对特征尺度敏感,通常需要缩放
    因为基于距离的算法依赖于特征之间的距离计算,尺度较大的特征可能会在距离计算中占据主导地位,即使它们的重要性并非更高。缩放可以确保所有特征对距离计算的贡献更加均衡。
  • 树模型(如 决策树、随机森林):对尺度不敏感,但对异常值和分布可能敏感。
    树模型基于单个特征内部值的顺序进行决策,因此对整体尺度不敏感

Python实现示例

Pandas的isnull()sum()方法来检查缺失值,使用fillna()方法进行填充,使用dropna()方法删除缺失值。
可以使用Matplotlib和Seaborn库进行可视化以检测异常值,并使用NumPy进行统计计算。
可以使用sklearn.preprocessing模块中的各种类进行数据缩放、归一化和编码。

一个简单的数据清洗工作流程包括以下步骤:

首先,加载数据并使用Pandas进行初步的探索性分析,例如查看数据的基本统计信息和缺失值情况。
然后,根据数据的特性和业务需求,选择合适的缺失值处理方法,并使用Pandas或scikit-learn进行填充或删除。
接下来,可以使用可视化方法和统计方法检测异常值,并根据情况选择删除、转换或保留。
最后,根据所选的机器学习算法的要求,对数据进行缩放、归一化或编码等转换。

下面展示一个端到端的数据清洗示例,包含:加载数据、检查/处理缺失值、检测/处理异常值、数据转换。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from sklearn.impute import SimpleImputer, KNNImputer
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.ensemble import IsolationForest

# 1. 加载示例数据
df = pd.DataFrame({
'age': [25, 30, np.nan, 22, 40, 120, 28],
'income': [50000, 60000, 55000, np.nan, 65000, 70000, 58000],
'gender': ['M', 'F', 'F', 'M', np.nan, 'M', 'F']
})

# 2. 初步探索
print("缺失值情况:")
print(df.isnull().sum())

# 3. 处理缺失值
# 3.1 对数值特征使用均值填充
num_cols = ['age', 'income']
imp_mean = SimpleImputer(strategy='mean')
df[num_cols] = imp_mean.fit_transform(df[num_cols])

# 3.2 对类别特征使用众数填充
imp_mode = SimpleImputer(strategy='most_frequent')
df[['gender']] = imp_mode.fit_transform(df[['gender']])

print("\n填充后数据:")
print(df)

# 4. 检测异常值(Isolation Forest)
iso = IsolationForest(contamination=0.15, random_state=42)
df[['age', 'income']] = df[['age', 'income']].astype(float)
iso.fit(df[['age', 'income']])
df['anomaly_flag'] = iso.predict(df[['age', 'income']]) # -1 异常,1 正常

print("\n异常检测结果:")
print(df[df['anomaly_flag'] == -1])

# 5. 处理异常值(这里以删除为例)
df_clean = df[df['anomaly_flag'] == 1].drop(columns=['anomaly_flag'])
print("\n删除异常值后的数据:")
print(df_clean)

# 6. 数据转换
# 6.1 连续特征标准化
scaler = StandardScaler()
df_clean[['age', 'income']] = scaler.fit_transform(df_clean[['age', 'income']])

# 6.2 类别特征独热编码
encoder = OneHotEncoder(sparse=False, handle_unknown='ignore')
gender_encoded = encoder.fit_transform(df_clean[['gender']])
df_gender = pd.DataFrame(gender_encoded, columns=encoder.get_feature_names_out(['gender']))

# 合并特征
df_final = pd.concat([df_clean.drop(columns=['gender']).reset_index(drop=True),
df_gender.reset_index(drop=True)], axis=1)

print("\n最终处理后数据:")
print(df_final)

注释解读:

  1. 使用 SimpleImputer 分别对数值特征(均值)和类别特征(众数)进行填充。
  2. IsolationForest 标记异常值,并将异常样本剔除。
  3. 对数值特征进行标准化、类别特征进行独热编码,得到最终可用于建模的数据集。

总结

  • 数据清洗是机器学习流程中的核心步骤,良好的数据清洗能显著提升模型质量与稳定性。
  • 针对 缺失值 要区分 MCAR、MAR、MNAR,不盲目删除或填充。
  • 异常值 的处理需结合业务背景,可能选择删除、转换、填充或保留。
  • 数据转换(缩放、编码)应根据所用算法特点进行选择。
  • 在 Python 中,Pandas、NumPy、Scikit-learn 等库提供了便捷的接口,能够高效地完成各类数据清洗操作。