引言:数据清洗的重要性

在数据科学和分析领域,数据清洗是整个工作流程中最为关键的一步。根据业界的经验,数据科学家通常花费60-80%的时间在数据清洗和预处理上。这是因为”垃圾进,垃圾出”(Garbage In, Garbage Out)的原则:无论你的分析模型多么先进,如果输入的数据质量低下,结果都将是不可靠的。

数据清洗不仅仅是删除缺失值那么简单,它是一个系统性的过程,包括识别和处理缺失值、异常值、重复数据、数据格式标准化、数据类型转换等多个方面。一个完整的数据清洗流程能够显著提高后续分析的准确性和可靠性。

本文将详细介绍如何使用Python及其强大的数据处理库(主要是pandas)进行高效的数据清洗。我们将通过一个完整的实际案例,展示从原始数据加载到最终分析就绪数据的全过程。

1. 数据清洗的基础环境搭建

1.1 必要的Python库

在开始数据清洗之前,我们需要安装并导入以下核心库:

# 数据处理核心库
import pandas as pd
import numpy as np

# 数据可视化
import matplotlib.pyplot as plt
import seaborn as sns

# 正则表达式处理
import re

# 日期时间处理
from datetime import datetime

# 设置显示选项
pd.set_option('display.max_columns', None)
pd.set_option('display.max_rows', 100)
plt.style.use('seaborn-v0_8')

1.2 创建示例数据集

为了更好地演示数据清洗过程,我们将创建一个包含各种常见数据问题的示例数据集:

# 创建一个包含各种数据问题的示例数据集
data = {
    'customer_id': [1001, 1002, 1003, 1004, 1005, 1006, 1007, 1008, 1009, 1010],
    'name': ['John Doe', 'Jane Smith', 'Bob Johnson', 'Alice Brown', 'Charlie Wilson', 
             'David Lee', 'Eva Martinez', 'Frank White', 'Grace Lee', 'Henry Davis'],
    'age': [25, 30, 35, 28, 42, 29, 33, 27, 31, 38],
    'email': ['john.doe@email.com', 'jane.smith@email.com', 'bob.johnson@email.com', 
              'alice.brown@email.com', 'charlie.wilson@email.com', 'david.lee@email.com',
              'eva.martinez@email.com', 'frank.white@email.com', 'grace.lee@email.com',
              'henry.davis@email.com'],
    'phone': ['555-1234', '555-5678', '555-9012', '555-3456', '555-7890', 
              '555-2345', '555-6789', '555-0123', '555-4567', '555-8901'],
    'purchase_date': ['2023-01-15', '2023-02-20', '2023-01-18', '2023-03-25', '2023-02-14',
                      '2023-01-30', '2023-03-05', '2023-02-28', '2023-03-12', '2023-01-22'],
    'amount': [125.50, 200.00, 89.99, 150.75, 300.00, 95.50, 175.25, 225.00, 110.00, 250.00],
    'status': ['Completed', 'Pending', 'Completed', 'Cancelled', 'Completed', 
               'Pending', 'Completed', 'Completed', 'Pending', 'Completed'],
    'country': ['USA', 'UK', 'USA', 'Canada', 'USA', 'UK', 'Canada', 'USA', 'UK', 'Canada']
}

df = pd.DataFrame(data)

# 添加一些数据质量问题
# 1. 重复数据
df_duplicate = df.iloc[[0]].copy()
df_duplicate['customer_id'] = 1011
df_duplicate['name'] = 'John Doe'
df_duplicate['email'] = 'john.doe@email.com'
df_duplicate['amount'] = 125.50
df = pd.concat([df, df_duplicate], ignore_index=True)

# 2. 缺失值
df.loc[2, 'age'] = np.nan
df.loc[4, 'email'] = np.nan
df.loc[6, 'phone'] = np.nan

# 3. 异常值
df.loc[8, 'age'] = 200  # 不合理的年龄
df.loc[9, 'amount'] = -50  # 负的金额

# 4. 格式不一致
df.loc[1, 'phone'] = '555 5678'  # 格式不一致
df.loc[3, 'purchase_date'] = '03/25/2023'  # 日期格式不一致

# 5. 大小写不一致
df.loc[5, 'country'] = 'usa'  # 小写
df.loc[7, 'country'] = 'UK '  # 多余空格

print("原始数据集预览:")
print(df)

2. 数据探索与质量评估

在开始清洗之前,我们需要先了解数据的整体情况,识别存在的问题。

2.1 基本信息检查

# 查看数据基本信息
print("数据集基本信息:")
print(df.info())

# 查看数据的统计描述
print("\n数据统计描述:")
print(df.describe())

# 检查缺失值
print("\n缺失值统计:")
print(df.isnull().sum())

# 检查重复值
print("\n重复值数量:", df.duplicated().sum())

2.2 数据质量可视化

# 创建一个子图来显示数据质量情况
fig, axes = plt.subplots(2, 2, figsize=(15, 10))

# 缺失值热图
sns.heatmap(df.isnull(), cbar=False, ax=axes[0,0])
axes[0,0].set_title('缺失值分布')

# 数值型数据分布
df_numeric = df.select_dtypes(include=[np.number])
if not df_numeric.empty:
    df_numeric.hist(bins=15, ax=axes[0,1])
    axes[0,1].set_title('数值型数据分布')

# 类别型数据计数
df_categorical = df.select_dtypes(include=['object'])
if not df_categorical.empty:
    categorical_counts = df_categorical.apply(lambda x: x.nunique())
    categorical_counts.plot(kind='bar', ax=axes[1,0])
    axes[1,0].set_title('类别型数据唯一值数量')
    axes[1,0].tick_params(axis='x', rotation=45)

# 数据类型分布
df.dtypes.value_counts().plot(kind='pie', ax=axes[1,1])
axes[1,1].set_title('数据类型分布')

plt.tight_layout()
plt.show()

3. 处理重复数据

重复数据是数据集中常见的问题,会导致分析结果偏差。

3.1 识别重复数据

# 查看重复数据
print("重复数据行:")
print(df[df.duplicated()])

# 查看基于特定列的重复数据
print("\n基于customer_id的重复数据:")
print(df[df.duplicated(subset=['customer_id'], keep=False)])

3.2 删除重复数据

# 删除完全重复的行
df_cleaned = df.drop_duplicates()

# 删除基于特定列的重复数据,保留最后一条记录
df_cleaned = df_cleaned.drop_duplicates(subset=['customer_id'], keep='last')

print(f"原始数据行数: {len(df)}")
print(f"清洗后数据行数: {len(df_cleaned)}")

4. 处理缺失值

缺失值处理是数据清洗中最常见的任务之一。

4.1 缺失值识别与分析

# 计算每列的缺失值比例
missing_data = df_cleaned.isnull().sum()
missing_percentage = (missing_data / len(df_cleaned)) * 100

print("缺失值统计:")
print(pd.DataFrame({
    '缺失数量': missing_data,
    '缺失比例(%)': missing_percentage.round(2)
}))

4.2 缺失值处理策略

根据不同的数据类型和业务场景,我们采用不同的处理方法:

# 1. 删除缺失值(适用于缺失比例很小的情况)
df_dropped = df_cleaned.dropna(subset=['age'])

# 2. 填充缺失值
# 数值型:使用中位数填充(对异常值不敏感)
df_cleaned['age'] = df_cleaned['age'].fillna(df_cleaned['age'].median())

# 类别型:使用众数填充
df_cleaned['email'] = df_cleaned['email'].fillna(df_cleaned['email'].mode()[0])

# 3. 特殊标记(适用于缺失本身有意义的情况)
df_cleaned['phone'] = df_cleaned['phone'].fillna('Unknown')

# 4. 前向填充或后向填充(适用于时间序列数据)
# df_cleaned['some_column'] = df_cleaned['some_column'].fillna(method='ffill')

print("\n处理后的缺失值统计:")
print(df_cleaned.isnull().sum())

5. 处理异常值

异常值可能由数据录入错误、测量误差或真实的极端值引起。

5.1 异常值检测

# 使用箱线图检测异常值
def detect_outliers_iqr(data, column):
    Q1 = data[column].quantile(0.25)
    Q3 = data[column].quantile(0.75)
    IQR = Q3 - Q1
    lower_bound = Q1 - 1.5 * IQR
    upper_bound = Q3 + 1.5 * IQR
    outliers = data[(data[column] < lower_bound) | (data[column] > upper_bound)]
    return outliers, lower_bound, upper_bound

# 检测年龄异常值
age_outliers, age_lower, age_upper = detect_outliers_iqr(df_cleaned, 'age')
print("年龄异常值:")
print(age_outliers)

# 检测金额异常值
amount_outliers, amount_lower, amount_upper = detect_outliers_iqr(df_cleaned, 'amount')
print("\n金额异常值:")
print(amount_outliers)

5.2 异常值处理

# 方法1:删除异常值(适用于明显的错误数据)
df_cleaned = df_cleaned[(df_cleaned['age'] >= age_lower) & (df_cleaned['age'] <= age_upper)]
df_cleaned = df_cleaned[(df_cleaned['amount'] >= amount_lower) & (df_cleaned['amount'] <= amount_upper)]

# 方法2:截断异常值(Winsorization)
# df_cleaned['age'] = np.clip(df_cleaned['age'], age_lower, age_upper)

# 方法3:替换为边界值
# df_cleaned.loc[df_cleaned['age'] > age_upper, 'age'] = age_upper
# df_cleaned.loc[df_cleaned['age'] < age_lower, 'age'] = age_lower

print(f"处理异常值后剩余数据行数: {len(df_cleaned)}")

6. 数据格式标准化

6.1 文本数据标准化

# 姓名标准化:去除多余空格,统一大小写
df_cleaned['name'] = df_cleaned['name'].str.strip().str.title()

# 电话号码标准化:统一格式
def standardize_phone(phone):
    if phone == 'Unknown':
        return phone
    # 移除所有非数字字符
    digits = re.sub(r'\D', '', phone)
    # 格式化为 XXX-XXXX
    if len(digits) == 7:
        return f"{digits[:3]}-{digits[3:]}"
    elif len(digits) == 10:
        return f"{digits[:3]}-{digits[3:]}"
    else:
        return phone

df_cleaned['phone'] = df_cleaned['phone'].apply(standardize_phone)

# 邮箱标准化:统一大小写
df_cleaned['email'] = df_cleaned['email'].str.lower().str.strip()

# 国家标准化:统一格式
country_mapping = {
    'usa': 'USA',
    'UK ': 'UK',
    'uk': 'UK',
    'canada': 'Canada'
}
df_cleaned['country'] = df_cleaned['country'].str.strip().str.title()
df_cleaned['country'] = df_cleaned['country'].replace(country_mapping)

print("\n标准化后的数据:")
print(df_cleaned[['name', 'phone', 'email', 'country']].head())

6.2 日期格式标准化

# 统一日期格式
def standardize_date(date_str):
    try:
        # 尝试解析多种日期格式
        for fmt in ['%Y-%m-%d', '%m/%d/%Y', '%d-%m-%Y']:
            try:
                return datetime.strptime(date_str, fmt).strftime('%Y-%m-%d')
            except ValueError:
                continue
        return date_str
    except:
        return date_str

df_cleaned['purchase_date'] = df_cleaned['purchase_date'].apply(standardize_date)

# 转换为日期类型
df_cleaned['purchase_date'] = pd.to_datetime(df_cleaned['purchase_date'])

print("\n日期标准化结果:")
print(df_cleaned[['purchase_date']].head())

7. 数据类型转换与优化

7.1 数据类型检查与转换

# 查看当前数据类型
print("当前数据类型:")
print(df_cleaned.dtypes)

# 优化数据类型以节省内存
# 将数值型转换为更小的类型
df_cleaned['customer_id'] = df_cleaned['customer_id'].astype('int32')
df_cleaned['age'] = df_cleaned['age'].astype('int32')
df_cleaned['amount'] = df_cleaned['amount'].astype('float32')

# 将类别型数据转换为category类型
df_cleaned['status'] = df_cleaned['status'].astype('category')
df_cleaned['country'] = df_cleaned['country'].astype('category')

print("\n优化后的数据类型:")
print(df_cleaned.dtypes)

# 内存使用对比
print(f"\n优化前内存估计: {df.memory_usage(deep=True).sum() / 1024:.2f} KB")
print(f"优化后内存估计: {df_cleaned.memory_usage(deep=True).sum() / 1024:.2f} KB")

8. 特征工程与数据增强

8.1 创建新特征

# 从日期中提取特征
df_cleaned['purchase_year'] = df_cleaned['purchase_date'].dt.year
df_cleaned['purchase_month'] = df_cleaned['purchase_date'].dt.month
df_cleaned['purchase_day'] = df_cleaned['purchase_date'].dt.day
df_cleaned['purchase_weekday'] = df_cleaned['purchase_date'].dt.weekday

# 创建年龄分组
def age_group(age):
    if age < 30:
        return 'Young'
    elif age < 40:
        return 'Middle'
    else:
        return 'Senior'

df_cleaned['age_group'] = df_cleaned['age'].apply(age_group)

# 创建金额等级
def amount_level(amount):
    if amount < 100:
        return 'Low'
    elif amount < 200:
        return 'Medium'
    else:
        return 'High'

df_cleaned['amount_level'] = df_cleaned['amount'].apply(amount_level)

# 提取邮箱域名
df_cleaned['email_domain'] = df_cleaned['email'].str.split('@').str[1]

print("\n特征工程后的数据:")
print(df_cleaned[['purchase_year', 'purchase_month', 'age_group', 'amount_level', 'email_domain']].head())

8.2 数据编码转换

# 对类别型数据进行编码
from sklearn.preprocessing import LabelEncoder

# 为机器学习准备数据
df_ml = df_cleaned.copy()

# 标签编码
label_encoders = {}
categorical_columns = ['status', 'country', 'age_group', 'amount_level', 'email_domain']

for col in categorical_columns:
    le = LabelEncoder()
    df_ml[col + '_encoded'] = le.fit_transform(df_ml[col].astype(str))
    label_encoders[col] = le

# 独热编码(适用于类别数较少的情况)
df_ml = pd.get_dummies(df_ml, columns=['status', 'country'], prefix=['status', 'country'])

print("\n机器学习准备数据:")
print(df_ml[['status_encoded', 'country_encoded', 'age_group_encoded', 'amount_level_encoded']].head())

9. 数据验证与质量检查

9.1 验证规则定义

# 定义数据验证规则
validation_rules = {
    'customer_id': {'min': 1000, 'max': 9999, 'type': 'integer'},
    'age': {'min': 18, 'max': 100, 'type': 'integer'},
    'amount': {'min': 0, 'max': 10000, 'type': 'float'},
    'email': {'pattern': r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$', 'type': 'string'},
    'phone': {'pattern': r'^\d{3}-\d{4}$', 'type': 'string'},
    'purchase_date': {'type': 'datetime'}
}

def validate_data(data, rules):
    validation_results = {}
    
    for column, rule in rules.items():
        if column not in data.columns:
            continue
            
        errors = []
        
        # 类型检查
        if rule['type'] == 'integer':
            if not pd.api.types.is_integer_dtype(data[column]):
                errors.append(f"数据类型错误: 期望整数")
        elif rule['type'] == 'float':
            if not pd.api.types.is_float_dtype(data[column]):
                errors.append(f"数据类型错误: 期望浮点数")
        elif rule['type'] == 'datetime':
            if not pd.api.types.is_datetime64_any_dtype(data[column]):
                errors.append(f"数据类型错误: 期望日期时间")
        
        # 范围检查
        if 'min' in rule:
            invalid_min = data[data[column] < rule['min']]
            if len(invalid_min) > 0:
                errors.append(f"存在小于最小值 {rule['min']} 的数据: {len(invalid_min)} 条")
        
        if 'max' in rule:
            invalid_max = data[data[column] > rule['max']]
            if len(invalid_max) > 0:
                errors.append(f"存在大于最大值 {rule['max']} 的数据: {len(invalid_max)} 条")
        
        # 模式检查
        if 'pattern' in rule:
            invalid_pattern = data[~data[column].astype(str).str.match(rule['pattern'], na=False)]
            if len(invalid_pattern) > 0:
                errors.append(f"存在不符合模式的数据: {len(invalid_pattern)} 条")
        
        validation_results[column] = errors if errors else ["通过"]
    
    return validation_results

# 执行验证
validation_results = validate_data(df_cleaned, validation_rules)

print("数据验证结果:")
for column, results in validation_results.items():
    print(f"{column}:")
    for result in results:
        print(f"  - {result}")

10. 完整的数据清洗函数

10.1 封装清洗流程

def comprehensive_data_cleaning(df, verbose=True):
    """
    综合数据清洗函数
    
    参数:
    df: 原始DataFrame
    verbose: 是否显示详细信息
    
    返回:
    清洗后的DataFrame
    """
    df_clean = df.copy()
    
    if verbose:
        print(f"原始数据形状: {df_clean.shape}")
    
    # 1. 删除完全重复的行
    initial_rows = len(df_clean)
    df_clean = df_clean.drop_duplicates()
    if verbose:
        print(f"删除重复数据: {initial_rows - len(df_clean)} 行")
    
    # 2. 处理缺失值
    # 数值型用中位数填充
    numeric_cols = df_clean.select_dtypes(include=[np.number]).columns
    for col in numeric_cols:
        if df_clean[col].isnull().sum() > 0:
            median_val = df_clean[col].median()
            df_clean[col].fillna(median_val, inplace=True)
            if verbose:
                print(f"填充 {col} 的缺失值: {median_val}")
    
    # 类别型用众数或'Unknown'填充
    categorical_cols = df_clean.select_dtypes(include=['object']).columns
    for col in categorical_cols:
        if df_clean[col].isnull().sum() > 0:
            if col == 'phone':
                df_clean[col].fillna('Unknown', inplace=True)
            else:
                mode_val = df_clean[col].mode()[0] if not df_clean[col].mode().empty else 'Unknown'
                df_clean[col].fillna(mode_val, inplace=True)
            if verbose:
                print(f"填充 {col} 的缺失值")
    
    # 3. 处理异常值(使用IQR方法)
    for col in numeric_cols:
        Q1 = df_clean[col].quantile(0.25)
        Q3 = df_clean[col].quantile(0.75)
        IQR = Q3 - Q1
        lower_bound = Q1 - 1.5 * IQR
        upper_bound = Q3 + 1.5 * IQR
        
        # 截断异常值
        df_clean[col] = np.clip(df_clean[col], lower_bound, upper_bound)
        
        if verbose:
            outliers = len(df_clean[(df_clean[col] < lower_bound) | (df_clean[col] > upper_bound)])
            if outliers > 0:
                print(f"处理 {col} 的异常值: {outliers} 条")
    
    # 4. 文本标准化
    text_columns = df_clean.select_dtypes(include=['object']).columns
    for col in text_columns:
        # 去除首尾空格
        df_clean[col] = df_clean[col].astype(str).str.strip()
        
        # 如果是名称或邮箱,转换为小写和标题格式
        if 'name' in col.lower():
            df_clean[col] = df_clean[col].str.title()
        elif 'email' in col.lower():
            df_clean[col] = df_clean[col].str.lower()
    
    # 5. 数据类型优化
    # 数值型优化
    for col in numeric_cols:
        if df_clean[col].dtype == 'float64':
            df_clean[col] = df_clean[col].astype('float32')
        elif df_clean[col].dtype == 'int64':
            df_clean[col] = df_clean[col].astype('int32')
    
    # 类别型优化
    for col in categorical_cols:
        if df_clean[col].nunique() < len(df_clean) * 0.5:  # 如果唯一值少于50%
            df_clean[col] = df_clean[col].astype('category')
    
    if verbose:
        print(f"最终数据形状: {df_clean.shape}")
        print(f"内存使用优化: {df_clean.memory_usage(deep=True).sum() / 1024:.2f} KB")
    
    return df_clean

# 使用完整清洗函数
df_final = comprehensive_data_cleaning(df, verbose=True)

11. 数据清洗的性能优化技巧

11.1 使用向量化操作

# 避免使用循环,使用向量化操作
# 不好的做法
def slow_function(row):
    return row['amount'] * 0.1

# df['tax'] = df.apply(slow_function, axis=1)  # 慢

# 好的做法
df_final['tax'] = df_final['amount'] * 0.1  # 快

# 使用np.where进行条件操作
df_final['discount'] = np.where(
    df_final['amount'] > 200, 
    df_final['amount'] * 0.1, 
    0
)

11.2 使用chunk处理大数据

# 对于大数据集,分块处理
def process_large_file(file_path, chunk_size=10000):
    cleaned_chunks = []
    
    for chunk in pd.read_csv(file_path, chunksize=chunk_size):
        # 对每个chunk应用清洗逻辑
        chunk_cleaned = comprehensive_data_cleaning(chunk, verbose=False)
        cleaned_chunks.append(chunk_cleaned)
    
    return pd.concat(cleaned_chunks, ignore_index=True)

# 示例:处理大文件
# df_large = process_large_file('large_dataset.csv')

12. 数据清洗的验证与文档化

12.1 清洗过程记录

# 创建清洗日志
cleaning_log = {
    'original_shape': df.shape,
    'final_shape': df_final.shape,
    'steps': []
}

# 记录每个步骤
def log_step(log, step_name, before, after, description):
    log['steps'].append({
        'step': step_name,
        'rows_before': len(before),
        'rows_after': len(after),
        'description': description
    })

# 使用日志记录
log_step(cleaning_log, 'Initial', df, df, '原始数据')
log_step(cleaning_log, 'Remove Duplicates', df, df.drop_duplicates(), '删除重复行')
log_step(cleaning_log, 'Final', df.drop_duplicates(), df_final, '完成清洗')

print("\n清洗过程日志:")
for step in cleaning_log['steps']:
    print(f"{step['step']}: {step['rows_before']} -> {step['rows_after']} ({step['description']})")

13. 实际应用案例:销售数据分析

13.1 完整案例演示

# 创建一个更复杂的销售数据集
sales_data = {
    'order_id': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12],
    'customer_name': ['John Doe', 'Jane Smith', 'Bob Johnson', 'Alice Brown', 'Charlie Wilson',
                      'David Lee', 'Eva Martinez', 'Frank White', 'Grace Lee', 'Henry Davis',
                      'John Doe', 'Ivy Chen'],
    'product': ['Laptop', 'Mouse', 'Keyboard', 'Monitor', 'Laptop', 'Mouse', 'Keyboard', 
                'Monitor', 'Laptop', 'Mouse', 'Laptop', 'Keyboard'],
    'quantity': [1, 2, 1, 1, 2, 3, 1, 1, 1, 2, 1, 1],
    'unit_price': [1200, 25, 75, 300, 1200, 25, 75, 300, 1200, 25, 1200, 75],
    'order_date': ['2023-01-15', '2023-01-16', '2023-01-17', '2023-01-18', '2023-01-19',
                   '2023-01-20', '2023-01-21', '2023-01-22', '2023-01-23', '2023-01-24',
                   '2023-01-15', '2023-01-25'],
    'status': ['Completed', 'Pending', 'Completed', 'Cancelled', 'Completed', 
               'Pending', 'Completed', 'Completed', 'Pending', 'Completed', 
               'Completed', 'Pending'],
    'region': ['North', 'South', 'East', 'West', 'North', 'South', 'East', 'West', 
               'North', 'South', 'North', 'East']
}

sales_df = pd.DataFrame(sales_data)

# 添加数据问题
sales_df.loc[2, 'quantity'] = -1  # 负数量
sales_df.loc[4, 'unit_price'] = 0  # 零价格
sales_df.loc[6, 'order_date'] = '2023-13-01'  # 无效日期
sales_df.loc[8, 'status'] = 'COMPLETED'  # 大小写不一致
sales_df.loc[10, 'region'] = 'north'  # 小写
sales_df = pd.concat([sales_df, sales_df.iloc[[0]].copy()], ignore_index=True)  # 重复

print("原始销售数据:")
print(sales_df)

# 应用清洗函数
def clean_sales_data(df):
    # 删除重复
    df = df.drop_duplicates()
    
    # 处理负数量和零价格
    df['quantity'] = df['quantity'].clip(lower=1)
    df['unit_price'] = df['unit_price'].clip(lower=1)
    
    # 日期标准化
    df['order_date'] = pd.to_datetime(df['order_date'], errors='coerce')
    df = df.dropna(subset=['order_date'])
    
    # 状态标准化
    df['status'] = df['status'].str.title()
    
    # 地区标准化
    df['region'] = df['region'].str.title()
    
    # 计算总金额
    df['total_amount'] = df['quantity'] * df['unit_price']
    
    return df

sales_clean = clean_sales_data(sales_df)

print("\n清洗后的销售数据:")
print(sales_clean)

# 分析结果
print("\n销售分析:")
print(f"总订单数: {len(sales_clean)}")
print(f"总销售额: ${sales_clean['total_amount'].sum():.2f}")
print(f"平均订单金额: ${sales_clean['total_amount'].mean():.2f}")
print("\n按状态统计:")
print(sales_clean['status'].value_counts())
print("\n按地区统计:")
print(sales_clean.groupby('region')['total_amount'].sum())

14. 数据清洗的最佳实践

14.1 清洗原则

  1. 保持原始数据不变:始终在数据的副本上操作
  2. 记录所有更改:保存清洗日志,便于追溯
  3. 分步骤验证:每步清洗后检查数据质量
  4. 自动化重复任务:编写可重用的函数
  5. 考虑业务逻辑:清洗要符合业务规则

14.2 常见陷阱与避免方法

# 陷阱1:链式赋值导致的SettingWithCopyWarning
# 错误做法
# df_clean = df[df['amount'] > 0]
# df_clean['new_col'] = ...  # 可能产生警告

# 正确做法
df_clean = df[df['amount'] > 0].copy()

# 陷阱2:在循环中频繁修改DataFrame
# 错误做法
# for i in range(len(df)):
#     df.loc[i, 'new_col'] = ...  # 慢且危险

# 正确做法
# df['new_col'] = df.apply(lambda row: ..., axis=1)  # 或向量化操作

# 陷阱3:忽略数据类型导致的内存浪费
# 错误做法
# df['category'] = df['category'].astype('object')  # 浪费内存

# 正确做法
# df['category'] = df['category'].astype('category')  # 节省内存

15. 总结

数据清洗是数据分析过程中不可或缺的一步,它直接影响着分析结果的质量。通过本文的详细介绍,我们学习了:

  1. 系统性的清洗流程:从数据探索、问题识别到具体处理
  2. 多种处理策略:针对不同类型的问题采用不同的方法
  3. 性能优化技巧:向量化操作、内存优化等
  4. 验证与文档化:确保清洗过程的可追溯性
  5. 实际应用案例:将理论应用到真实场景中

记住,好的数据清洗不仅仅是技术问题,更需要对业务逻辑的深入理解。在实际工作中,应该根据具体的数据特征和业务需求,灵活选择和组合不同的清洗方法,最终得到高质量、可用于分析的数据。

通过建立标准化的数据清洗流程和最佳实践,可以大大提高数据分析的效率和可靠性,为后续的机器学习建模和业务决策奠定坚实的基础。# 如何使用Python进行高效的数据清洗:从原始数据到分析就绪的完整指南

引言:数据清洗的重要性

在数据科学和分析领域,数据清洗是整个工作流程中最为关键的一步。根据业界的经验,数据科学家通常花费60-80%的时间在数据清洗和预处理上。这是因为”垃圾进,垃圾出”(Garbage In, Garbage Out)的原则:无论你的分析模型多么先进,如果输入的数据质量低下,结果都将是不可靠的。

数据清洗不仅仅是删除缺失值那么简单,它是一个系统性的过程,包括识别和处理缺失值、异常值、重复数据、数据格式标准化、数据类型转换等多个方面。一个完整的数据清洗流程能够显著提高后续分析的准确性和可靠性。

本文将详细介绍如何使用Python及其强大的数据处理库(主要是pandas)进行高效的数据清洗。我们将通过一个完整的实际案例,展示从原始数据加载到最终分析就绪数据的全过程。

1. 数据清洗的基础环境搭建

1.1 必要的Python库

在开始数据清洗之前,我们需要安装并导入以下核心库:

# 数据处理核心库
import pandas as pd
import numpy as np

# 数据可视化
import matplotlib.pyplot as plt
import seaborn as sns

# 正则表达式处理
import re

# 日期时间处理
from datetime import datetime

# 设置显示选项
pd.set_option('display.max_columns', None)
pd.set_option('display.max_rows', 100)
plt.style.use('seaborn-v0_8')

1.2 创建示例数据集

为了更好地演示数据清洗过程,我们将创建一个包含各种常见数据问题的示例数据集:

# 创建一个包含各种数据问题的示例数据集
data = {
    'customer_id': [1001, 1002, 1003, 1004, 1005, 1006, 1007, 1008, 1009, 1010],
    'name': ['John Doe', 'Jane Smith', 'Bob Johnson', 'Alice Brown', 'Charlie Wilson', 
             'David Lee', 'Eva Martinez', 'Frank White', 'Grace Lee', 'Henry Davis'],
    'age': [25, 30, 35, 28, 42, 29, 33, 27, 31, 38],
    'email': ['john.doe@email.com', 'jane.smith@email.com', 'bob.johnson@email.com', 
              'alice.brown@email.com', 'charlie.wilson@email.com', 'david.lee@email.com',
              'eva.martinez@email.com', 'frank.white@email.com', 'grace.lee@email.com',
              'henry.davis@email.com'],
    'phone': ['555-1234', '555-5678', '555-9012', '555-3456', '555-7890', 
              '555-2345', '555-6789', '555-0123', '555-4567', '555-8901'],
    'purchase_date': ['2023-01-15', '2023-02-20', '2023-01-18', '2023-03-25', '2023-02-14',
                      '2023-01-30', '2023-03-05', '2023-02-28', '2023-03-12', '2023-01-22'],
    'amount': [125.50, 200.00, 89.99, 150.75, 300.00, 95.50, 175.25, 225.00, 110.00, 250.00],
    'status': ['Completed', 'Pending', 'Completed', 'Cancelled', 'Completed', 
               'Pending', 'Completed', 'Completed', 'Pending', 'Completed'],
    'country': ['USA', 'UK', 'USA', 'Canada', 'USA', 'UK', 'Canada', 'USA', 'UK', 'Canada']
}

df = pd.DataFrame(data)

# 添加一些数据质量问题
# 1. 重复数据
df_duplicate = df.iloc[[0]].copy()
df_duplicate['customer_id'] = 1011
df_duplicate['name'] = 'John Doe'
df_duplicate['email'] = 'john.doe@email.com'
df_duplicate['amount'] = 125.50
df = pd.concat([df, df_duplicate], ignore_index=True)

# 2. 缺失值
df.loc[2, 'age'] = np.nan
df.loc[4, 'email'] = np.nan
df.loc[6, 'phone'] = np.nan

# 3. 异常值
df.loc[8, 'age'] = 200  # 不合理的年龄
df.loc[9, 'amount'] = -50  # 负的金额

# 4. 格式不一致
df.loc[1, 'phone'] = '555 5678'  # 格式不一致
df.loc[3, 'purchase_date'] = '03/25/2023'  # 日期格式不一致

# 5. 大小写不一致
df.loc[5, 'country'] = 'usa'  # 小写
df.loc[7, 'country'] = 'UK '  # 多余空格

print("原始数据集预览:")
print(df)

2. 数据探索与质量评估

在开始清洗之前,我们需要先了解数据的整体情况,识别存在的问题。

2.1 基本信息检查

# 查看数据基本信息
print("数据集基本信息:")
print(df.info())

# 查看数据的统计描述
print("\n数据统计描述:")
print(df.describe())

# 检查缺失值
print("\n缺失值统计:")
print(df.isnull().sum())

# 检查重复值
print("\n重复值数量:", df.duplicated().sum())

2.2 数据质量可视化

# 创建一个子图来显示数据质量情况
fig, axes = plt.subplots(2, 2, figsize=(15, 10))

# 缺失值热图
sns.heatmap(df.isnull(), cbar=False, ax=axes[0,0])
axes[0,0].set_title('缺失值分布')

# 数值型数据分布
df_numeric = df.select_dtypes(include=[np.number])
if not df_numeric.empty:
    df_numeric.hist(bins=15, ax=axes[0,1])
    axes[0,1].set_title('数值型数据分布')

# 类别型数据计数
df_categorical = df.select_dtypes(include=['object'])
if not df_categorical.empty:
    categorical_counts = df_categorical.apply(lambda x: x.nunique())
    categorical_counts.plot(kind='bar', ax=axes[1,0])
    axes[1,0].set_title('类别型数据唯一值数量')
    axes[1,0].tick_params(axis='x', rotation=45)

# 数据类型分布
df.dtypes.value_counts().plot(kind='pie', ax=axes[1,1])
axes[1,1].set_title('数据类型分布')

plt.tight_layout()
plt.show()

3. 处理重复数据

重复数据是数据集中常见的问题,会导致分析结果偏差。

3.1 识别重复数据

# 查看重复数据
print("重复数据行:")
print(df[df.duplicated()])

# 查看基于特定列的重复数据
print("\n基于customer_id的重复数据:")
print(df[df.duplicated(subset=['customer_id'], keep=False)])

3.2 删除重复数据

# 删除完全重复的行
df_cleaned = df.drop_duplicates()

# 删除基于特定列的重复数据,保留最后一条记录
df_cleaned = df_cleaned.drop_duplicates(subset=['customer_id'], keep='last')

print(f"原始数据行数: {len(df)}")
print(f"清洗后数据行数: {len(df_cleaned)}")

4. 处理缺失值

缺失值处理是数据清洗中最常见的任务之一。

4.1 缺失值识别与分析

# 计算每列的缺失值比例
missing_data = df_cleaned.isnull().sum()
missing_percentage = (missing_data / len(df_cleaned)) * 100

print("缺失值统计:")
print(pd.DataFrame({
    '缺失数量': missing_data,
    '缺失比例(%)': missing_percentage.round(2)
}))

4.2 缺失值处理策略

根据不同的数据类型和业务场景,我们采用不同的处理方法:

# 1. 删除缺失值(适用于缺失比例很小的情况)
df_dropped = df_cleaned.dropna(subset=['age'])

# 2. 填充缺失值
# 数值型:使用中位数填充(对异常值不敏感)
df_cleaned['age'] = df_cleaned['age'].fillna(df_cleaned['age'].median())

# 类别型:使用众数填充
df_cleaned['email'] = df_cleaned['email'].fillna(df_cleaned['email'].mode()[0])

# 3. 特殊标记(适用于缺失本身有意义的情况)
df_cleaned['phone'] = df_cleaned['phone'].fillna('Unknown')

# 4. 前向填充或后向填充(适用于时间序列数据)
# df_cleaned['some_column'] = df_cleaned['some_column'].fillna(method='ffill')

print("\n处理后的缺失值统计:")
print(df_cleaned.isnull().sum())

5. 处理异常值

异常值可能由数据录入错误、测量误差或真实的极端值引起。

5.1 异常值检测

# 使用箱线图检测异常值
def detect_outliers_iqr(data, column):
    Q1 = data[column].quantile(0.25)
    Q3 = data[column].quantile(0.75)
    IQR = Q3 - Q1
    lower_bound = Q1 - 1.5 * IQR
    upper_bound = Q3 + 1.5 * IQR
    outliers = data[(data[column] < lower_bound) | (data[column] > upper_bound)]
    return outliers, lower_bound, upper_bound

# 检测年龄异常值
age_outliers, age_lower, age_upper = detect_outliers_iqr(df_cleaned, 'age')
print("年龄异常值:")
print(age_outliers)

# 检测金额异常值
amount_outliers, amount_lower, amount_upper = detect_outliers_iqr(df_cleaned, 'amount')
print("\n金额异常值:")
print(amount_outliers)

5.2 异常值处理

# 方法1:删除异常值(适用于明显的错误数据)
df_cleaned = df_cleaned[(df_cleaned['age'] >= age_lower) & (df_cleaned['age'] <= age_upper)]
df_cleaned = df_cleaned[(df_cleaned['amount'] >= amount_lower) & (df_cleaned['amount'] <= amount_upper)]

# 方法2:截断异常值(Winsorization)
# df_cleaned['age'] = np.clip(df_cleaned['age'], age_lower, age_upper)

# 方法3:替换为边界值
# df_cleaned.loc[df_cleaned['age'] > age_upper, 'age'] = age_upper
# df_cleaned.loc[df_cleaned['age'] < age_lower, 'age'] = age_lower

print(f"处理异常值后剩余数据行数: {len(df_cleaned)}")

6. 数据格式标准化

6.1 文本数据标准化

# 姓名标准化:去除多余空格,统一大小写
df_cleaned['name'] = df_cleaned['name'].str.strip().str.title()

# 电话号码标准化:统一格式
def standardize_phone(phone):
    if phone == 'Unknown':
        return phone
    # 移除所有非数字字符
    digits = re.sub(r'\D', '', phone)
    # 格式化为 XXX-XXXX
    if len(digits) == 7:
        return f"{digits[:3]}-{digits[3:]}"
    elif len(digits) == 10:
        return f"{digits[:3]}-{digits[3:]}"
    else:
        return phone

df_cleaned['phone'] = df_cleaned['phone'].apply(standardize_phone)

# 邮箱标准化:统一大小写
df_cleaned['email'] = df_cleaned['email'].str.lower().str.strip()

# 国家标准化:统一格式
country_mapping = {
    'usa': 'USA',
    'UK ': 'UK',
    'uk': 'UK',
    'canada': 'Canada'
}
df_cleaned['country'] = df_cleaned['country'].str.strip().str.title()
df_cleaned['country'] = df_cleaned['country'].replace(country_mapping)

print("\n标准化后的数据:")
print(df_cleaned[['name', 'phone', 'email', 'country']].head())

6.2 日期格式标准化

# 统一日期格式
def standardize_date(date_str):
    try:
        # 尝试解析多种日期格式
        for fmt in ['%Y-%m-%d', '%m/%d/%Y', '%d-%m-%Y']:
            try:
                return datetime.strptime(date_str, fmt).strftime('%Y-%m-%d')
            except ValueError:
                continue
        return date_str
    except:
        return date_str

df_cleaned['purchase_date'] = df_cleaned['purchase_date'].apply(standardize_date)

# 转换为日期类型
df_cleaned['purchase_date'] = pd.to_datetime(df_cleaned['purchase_date'])

print("\n日期标准化结果:")
print(df_cleaned[['purchase_date']].head())

7. 数据类型转换与优化

7.1 数据类型检查与转换

# 查看当前数据类型
print("当前数据类型:")
print(df_cleaned.dtypes)

# 优化数据类型以节省内存
# 将数值型转换为更小的类型
df_cleaned['customer_id'] = df_cleaned['customer_id'].astype('int32')
df_cleaned['age'] = df_cleaned['age'].astype('int32')
df_cleaned['amount'] = df_cleaned['amount'].astype('float32')

# 将类别型数据转换为category类型
df_cleaned['status'] = df_cleaned['status'].astype('category')
df_cleaned['country'] = df_cleaned['country'].astype('category')

print("\n优化后的数据类型:")
print(df_cleaned.dtypes)

# 内存使用对比
print(f"\n优化前内存估计: {df.memory_usage(deep=True).sum() / 1024:.2f} KB")
print(f"优化后内存估计: {df_cleaned.memory_usage(deep=True).sum() / 1024:.2f} KB")

8. 特征工程与数据增强

8.1 创建新特征

# 从日期中提取特征
df_cleaned['purchase_year'] = df_cleaned['purchase_date'].dt.year
df_cleaned['purchase_month'] = df_cleaned['purchase_date'].dt.month
df_cleaned['purchase_day'] = df_cleaned['purchase_date'].dt.day
df_cleaned['purchase_weekday'] = df_cleaned['purchase_date'].dt.weekday

# 创建年龄分组
def age_group(age):
    if age < 30:
        return 'Young'
    elif age < 40:
        return 'Middle'
    else:
        return 'Senior'

df_cleaned['age_group'] = df_cleaned['age'].apply(age_group)

# 创建金额等级
def amount_level(amount):
    if amount < 100:
        return 'Low'
    elif amount < 200:
        return 'Medium'
    else:
        return 'High'

df_cleaned['amount_level'] = df_cleaned['amount'].apply(amount_level)

# 提取邮箱域名
df_cleaned['email_domain'] = df_cleaned['email'].str.split('@').str[1]

print("\n特征工程后的数据:")
print(df_cleaned[['purchase_year', 'purchase_month', 'age_group', 'amount_level', 'email_domain']].head())

8.2 数据编码转换

# 对类别型数据进行编码
from sklearn.preprocessing import LabelEncoder

# 为机器学习准备数据
df_ml = df_cleaned.copy()

# 标签编码
label_encoders = {}
categorical_columns = ['status', 'country', 'age_group', 'amount_level', 'email_domain']

for col in categorical_columns:
    le = LabelEncoder()
    df_ml[col + '_encoded'] = le.fit_transform(df_ml[col].astype(str))
    label_encoders[col] = le

# 独热编码(适用于类别数较少的情况)
df_ml = pd.get_dummies(df_ml, columns=['status', 'country'], prefix=['status', 'country'])

print("\n机器学习准备数据:")
print(df_ml[['status_encoded', 'country_encoded', 'age_group_encoded', 'amount_level_encoded']].head())

9. 数据验证与质量检查

9.1 验证规则定义

# 定义数据验证规则
validation_rules = {
    'customer_id': {'min': 1000, 'max': 9999, 'type': 'integer'},
    'age': {'min': 18, 'max': 100, 'type': 'integer'},
    'amount': {'min': 0, 'max': 10000, 'type': 'float'},
    'email': {'pattern': r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$', 'type': 'string'},
    'phone': {'pattern': r'^\d{3}-\d{4}$', 'type': 'string'},
    'purchase_date': {'type': 'datetime'}
}

def validate_data(data, rules):
    validation_results = {}
    
    for column, rule in rules.items():
        if column not in data.columns:
            continue
            
        errors = []
        
        # 类型检查
        if rule['type'] == 'integer':
            if not pd.api.types.is_integer_dtype(data[column]):
                errors.append(f"数据类型错误: 期望整数")
        elif rule['type'] == 'float':
            if not pd.api.types.is_float_dtype(data[column]):
                errors.append(f"数据类型错误: 期望浮点数")
        elif rule['type'] == 'datetime':
            if not pd.api.types.is_datetime64_any_dtype(data[column]):
                errors.append(f"数据类型错误: 期望日期时间")
        
        # 范围检查
        if 'min' in rule:
            invalid_min = data[data[column] < rule['min']]
            if len(invalid_min) > 0:
                errors.append(f"存在小于最小值 {rule['min']} 的数据: {len(invalid_min)} 条")
        
        if 'max' in rule:
            invalid_max = data[data[column] > rule['max']]
            if len(invalid_max) > 0:
                errors.append(f"存在大于最大值 {rule['max']} 的数据: {len(invalid_max)} 条")
        
        # 模式检查
        if 'pattern' in rule:
            invalid_pattern = data[~data[column].astype(str).str.match(rule['pattern'], na=False)]
            if len(invalid_pattern) > 0:
                errors.append(f"存在不符合模式的数据: {len(invalid_pattern)} 条")
        
        validation_results[column] = errors if errors else ["通过"]
    
    return validation_results

# 执行验证
validation_results = validate_data(df_cleaned, validation_rules)

print("数据验证结果:")
for column, results in validation_results.items():
    print(f"{column}:")
    for result in results:
        print(f"  - {result}")

10. 完整的数据清洗函数

10.1 封装清洗流程

def comprehensive_data_cleaning(df, verbose=True):
    """
    综合数据清洗函数
    
    参数:
    df: 原始DataFrame
    verbose: 是否显示详细信息
    
    返回:
    清洗后的DataFrame
    """
    df_clean = df.copy()
    
    if verbose:
        print(f"原始数据形状: {df_clean.shape}")
    
    # 1. 删除完全重复的行
    initial_rows = len(df_clean)
    df_clean = df_clean.drop_duplicates()
    if verbose:
        print(f"删除重复数据: {initial_rows - len(df_clean)} 行")
    
    # 2. 处理缺失值
    # 数值型用中位数填充
    numeric_cols = df_clean.select_dtypes(include=[np.number]).columns
    for col in numeric_cols:
        if df_clean[col].isnull().sum() > 0:
            median_val = df_clean[col].median()
            df_clean[col].fillna(median_val, inplace=True)
            if verbose:
                print(f"填充 {col} 的缺失值: {median_val}")
    
    # 类别型用众数或'Unknown'填充
    categorical_cols = df_clean.select_dtypes(include=['object']).columns
    for col in categorical_cols:
        if df_clean[col].isnull().sum() > 0:
            if col == 'phone':
                df_clean[col].fillna('Unknown', inplace=True)
            else:
                mode_val = df_clean[col].mode()[0] if not df_clean[col].mode().empty else 'Unknown'
                df_clean[col].fillna(mode_val, inplace=True)
            if verbose:
                print(f"填充 {col} 的缺失值")
    
    # 3. 处理异常值(使用IQR方法)
    for col in numeric_cols:
        Q1 = df_clean[col].quantile(0.25)
        Q3 = df_clean[col].quantile(0.75)
        IQR = Q3 - Q1
        lower_bound = Q1 - 1.5 * IQR
        upper_bound = Q3 + 1.5 * IQR
        
        # 截断异常值
        df_clean[col] = np.clip(df_clean[col], lower_bound, upper_bound)
        
        if verbose:
            outliers = len(df_clean[(df_clean[col] < lower_bound) | (df_clean[col] > upper_bound)])
            if outliers > 0:
                print(f"处理 {col} 的异常值: {outliers} 条")
    
    # 4. 文本标准化
    text_columns = df_clean.select_dtypes(include=['object']).columns
    for col in text_columns:
        # 去除首尾空格
        df_clean[col] = df_clean[col].astype(str).str.strip()
        
        # 如果是名称或邮箱,转换为小写和标题格式
        if 'name' in col.lower():
            df_clean[col] = df_clean[col].str.title()
        elif 'email' in col.lower():
            df_clean[col] = df_clean[col].str.lower()
    
    # 5. 数据类型优化
    # 数值型优化
    for col in numeric_cols:
        if df_clean[col].dtype == 'float64':
            df_clean[col] = df_clean[col].astype('float32')
        elif df_clean[col].dtype == 'int64':
            df_clean[col] = df_clean[col].astype('int32')
    
    # 类别型优化
    for col in categorical_cols:
        if df_clean[col].nunique() < len(df_clean) * 0.5:  # 如果唯一值少于50%
            df_clean[col] = df_clean[col].astype('category')
    
    if verbose:
        print(f"最终数据形状: {df_clean.shape}")
        print(f"内存使用优化: {df_clean.memory_usage(deep=True).sum() / 1024:.2f} KB")
    
    return df_clean

# 使用完整清洗函数
df_final = comprehensive_data_cleaning(df, verbose=True)

11. 数据清洗的性能优化技巧

11.1 使用向量化操作

# 避免使用循环,使用向量化操作
# 不好的做法
def slow_function(row):
    return row['amount'] * 0.1

# df['tax'] = df.apply(slow_function, axis=1)  # 慢

# 好的做法
df_final['tax'] = df_final['amount'] * 0.1  # 快

# 使用np.where进行条件操作
df_final['discount'] = np.where(
    df_final['amount'] > 200, 
    df_final['amount'] * 0.1, 
    0
)

11.2 使用chunk处理大数据

# 对于大数据集,分块处理
def process_large_file(file_path, chunk_size=10000):
    cleaned_chunks = []
    
    for chunk in pd.read_csv(file_path, chunksize=chunk_size):
        # 对每个chunk应用清洗逻辑
        chunk_cleaned = comprehensive_data_cleaning(chunk, verbose=False)
        cleaned_chunks.append(chunk_cleaned)
    
    return pd.concat(cleaned_chunks, ignore_index=True)

# 示例:处理大文件
# df_large = process_large_file('large_dataset.csv')

12. 数据清洗的验证与文档化

12.1 清洗过程记录

# 创建清洗日志
cleaning_log = {
    'original_shape': df.shape,
    'final_shape': df_final.shape,
    'steps': []
}

# 记录每个步骤
def log_step(log, step_name, before, after, description):
    log['steps'].append({
        'step': step_name,
        'rows_before': len(before),
        'rows_after': len(after),
        'description': description
    })

# 使用日志记录
log_step(cleaning_log, 'Initial', df, df, '原始数据')
log_step(cleaning_log, 'Remove Duplicates', df, df.drop_duplicates(), '删除重复行')
log_step(cleaning_log, 'Final', df.drop_duplicates(), df_final, '完成清洗')

print("\n清洗过程日志:")
for step in cleaning_log['steps']:
    print(f"{step['step']}: {step['rows_before']} -> {step['rows_after']} ({step['description']})")

13. 实际应用案例:销售数据分析

13.1 完整案例演示

# 创建一个更复杂的销售数据集
sales_data = {
    'order_id': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12],
    'customer_name': ['John Doe', 'Jane Smith', 'Bob Johnson', 'Alice Brown', 'Charlie Wilson',
                      'David Lee', 'Eva Martinez', 'Frank White', 'Grace Lee', 'Henry Davis',
                      'John Doe', 'Ivy Chen'],
    'product': ['Laptop', 'Mouse', 'Keyboard', 'Monitor', 'Laptop', 'Mouse', 'Keyboard', 
                'Monitor', 'Laptop', 'Mouse', 'Laptop', 'Keyboard'],
    'quantity': [1, 2, 1, 1, 2, 3, 1, 1, 1, 2, 1, 1],
    'unit_price': [1200, 25, 75, 300, 1200, 25, 75, 300, 1200, 25, 1200, 75],
    'order_date': ['2023-01-15', '2023-01-16', '2023-01-17', '2023-01-18', '2023-01-19',
                   '2023-01-20', '2023-01-21', '2023-01-22', '2023-01-23', '2023-01-24',
                   '2023-01-15', '2023-01-25'],
    'status': ['Completed', 'Pending', 'Completed', 'Cancelled', 'Completed', 
               'Pending', 'Completed', 'Completed', 'Pending', 'Completed', 
               'Completed', 'Pending'],
    'region': ['North', 'South', 'East', 'West', 'North', 'South', 'East', 'West', 
               'North', 'South', 'North', 'East']
}

sales_df = pd.DataFrame(sales_data)

# 添加数据问题
sales_df.loc[2, 'quantity'] = -1  # 负数量
sales_df.loc[4, 'unit_price'] = 0  # 零价格
sales_df.loc[6, 'order_date'] = '2023-13-01'  # 无效日期
sales_df.loc[8, 'status'] = 'COMPLETED'  # 大小写不一致
sales_df.loc[10, 'region'] = 'north'  # 小写
sales_df = pd.concat([sales_df, sales_df.iloc[[0]].copy()], ignore_index=True)  # 重复

print("原始销售数据:")
print(sales_df)

# 应用清洗函数
def clean_sales_data(df):
    # 删除重复
    df = df.drop_duplicates()
    
    # 处理负数量和零价格
    df['quantity'] = df['quantity'].clip(lower=1)
    df['unit_price'] = df['unit_price'].clip(lower=1)
    
    # 日期标准化
    df['order_date'] = pd.to_datetime(df['order_date'], errors='coerce')
    df = df.dropna(subset=['order_date'])
    
    # 状态标准化
    df['status'] = df['status'].str.title()
    
    # 地区标准化
    df['region'] = df['region'].str.title()
    
    # 计算总金额
    df['total_amount'] = df['quantity'] * df['unit_price']
    
    return df

sales_clean = clean_sales_data(sales_df)

print("\n清洗后的销售数据:")
print(sales_clean)

# 分析结果
print("\n销售分析:")
print(f"总订单数: {len(sales_clean)}")
print(f"总销售额: ${sales_clean['total_amount'].sum():.2f}")
print(f"平均订单金额: ${sales_clean['total_amount'].mean():.2f}")
print("\n按状态统计:")
print(sales_clean['status'].value_counts())
print("\n按地区统计:")
print(sales_clean.groupby('region')['total_amount'].sum())

14. 数据清洗的最佳实践

14.1 清洗原则

  1. 保持原始数据不变:始终在数据的副本上操作
  2. 记录所有更改:保存清洗日志,便于追溯
  3. 分步骤验证:每步清洗后检查数据质量
  4. 自动化重复任务:编写可重用的函数
  5. 考虑业务逻辑:清洗要符合业务规则

14.2 常见陷阱与避免方法

# 陷阱1:链式赋值导致的SettingWithCopyWarning
# 错误做法
# df_clean = df[df['amount'] > 0]
# df_clean['new_col'] = ...  # 可能产生警告

# 正确做法
df_clean = df[df['amount'] > 0].copy()

# 陷阱2:在循环中频繁修改DataFrame
# 错误做法
# for i in range(len(df)):
#     df.loc[i, 'new_col'] = ...  # 慢且危险

# 正确做法
# df['new_col'] = df.apply(lambda row: ..., axis=1)  # 或向量化操作

# 陷阱3:忽略数据类型导致的内存浪费
# 错误做法
# df['category'] = df['category'].astype('object')  # 浪费内存

# 正确做法
# df['category'] = df['category'].astype('category')  # 节省内存

15. 总结

数据清洗是数据分析过程中不可或缺的一步,它直接影响着分析结果的质量。通过本文的详细介绍,我们学习了:

  1. 系统性的清洗流程:从数据探索、问题识别到具体处理
  2. 多种处理策略:针对不同类型的问题采用不同的方法
  3. 性能优化技巧:向量化操作、内存优化等
  4. 验证与文档化:确保清洗过程的可追溯性
  5. 实际应用案例:将理论应用到真实场景中

记住,好的数据清洗不仅仅是技术问题,更需要对业务逻辑的深入理解。在实际工作中,应该根据具体的数据特征和业务需求,灵活选择和组合不同的清洗方法,最终得到高质量、可用于分析的数据。

通过建立标准化的数据清洗流程和最佳实践,可以大大提高数据分析的效率和可靠性,为后续的机器学习建模和业务决策奠定坚实的基础。