引言:数据清洗的重要性
在数据科学和分析领域,数据清洗是整个工作流程中最为关键的一步。根据业界的经验,数据科学家通常花费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 清洗原则
- 保持原始数据不变:始终在数据的副本上操作
- 记录所有更改:保存清洗日志,便于追溯
- 分步骤验证:每步清洗后检查数据质量
- 自动化重复任务:编写可重用的函数
- 考虑业务逻辑:清洗要符合业务规则
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. 总结
数据清洗是数据分析过程中不可或缺的一步,它直接影响着分析结果的质量。通过本文的详细介绍,我们学习了:
- 系统性的清洗流程:从数据探索、问题识别到具体处理
- 多种处理策略:针对不同类型的问题采用不同的方法
- 性能优化技巧:向量化操作、内存优化等
- 验证与文档化:确保清洗过程的可追溯性
- 实际应用案例:将理论应用到真实场景中
记住,好的数据清洗不仅仅是技术问题,更需要对业务逻辑的深入理解。在实际工作中,应该根据具体的数据特征和业务需求,灵活选择和组合不同的清洗方法,最终得到高质量、可用于分析的数据。
通过建立标准化的数据清洗流程和最佳实践,可以大大提高数据分析的效率和可靠性,为后续的机器学习建模和业务决策奠定坚实的基础。# 如何使用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 清洗原则
- 保持原始数据不变:始终在数据的副本上操作
- 记录所有更改:保存清洗日志,便于追溯
- 分步骤验证:每步清洗后检查数据质量
- 自动化重复任务:编写可重用的函数
- 考虑业务逻辑:清洗要符合业务规则
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. 总结
数据清洗是数据分析过程中不可或缺的一步,它直接影响着分析结果的质量。通过本文的详细介绍,我们学习了:
- 系统性的清洗流程:从数据探索、问题识别到具体处理
- 多种处理策略:针对不同类型的问题采用不同的方法
- 性能优化技巧:向量化操作、内存优化等
- 验证与文档化:确保清洗过程的可追溯性
- 实际应用案例:将理论应用到真实场景中
记住,好的数据清洗不仅仅是技术问题,更需要对业务逻辑的深入理解。在实际工作中,应该根据具体的数据特征和业务需求,灵活选择和组合不同的清洗方法,最终得到高质量、可用于分析的数据。
通过建立标准化的数据清洗流程和最佳实践,可以大大提高数据分析的效率和可靠性,为后续的机器学习建模和业务决策奠定坚实的基础。
