Python数据分析实战:Pandas数据清洗与特征工程全流程(附完整代码)
·
Python数据分析实战:Pandas数据清洗与特征工程全流程
数据清洗和特征工程通常占据数据分析师60%-80%的工作时间。这篇文章用一个真实场景(电商用户行为数据),带你走通从脏数据到建模就绪数据的完整流程。
一、数据背景
假设我们有一份电商平台的用户行为日志,包含以下字段:
- user_id:用户ID
- event_type:行为类型(view/add_cart/purchase)
- product_id:商品ID
- price:商品价格
- timestamp:行为发生时间
- user_age:用户年龄
- city:用户城市
- device:设备类型
这份数据存在缺失值、异常值、重复记录等问题。我们的目标是:清理数据并构建可用于用户转化预测的特征矩阵。
二、数据加载与初步探索
import pandas as pd
import numpy as np
from datetime import datetime
import warnings
warnings.filterwarnings('ignore')
# 加载数据
df = pd.read_csv('user_behavior.csv', parse_dates=['timestamp'])
print(f"数据集形状: {df.shape}")
print(f"\n数据类型:\n{df.dtypes}")
print(f"\n缺失值统计:\n{df.isnull().sum()}")
print(f"\n基本统计:\n{df.describe()}")
三、缺失值处理
3.1 缺失值诊断
在动手填充之前,先判断缺失模式。缺失值分三种:
- 完全随机缺失(MCAR):缺失与任何变量无关
- 随机缺失(MAR):缺失与其他变量有关
- 非随机缺失(MNAR):缺失与自身值有关
# 创建缺失值指示矩阵
missing_matrix = df.isnull().astype(int)
# 检查缺失值之间的相关性
missing_corr = missing_matrix.corr()
print("缺失值相关性矩阵:")
print(missing_corr[missing_corr > 0.3])
# 按城市分组查看 price 缺失率
price_missing_by_city = df.groupby('city')['price'].apply(
lambda x: x.isnull().mean()
).sort_values(ascending=False)
print(f"\n各城市price缺失率:\n{price_missing_by_city.head(10)}")
3.2 分策略填充
# 策略1:价格用中位数填充(受异常值影响小)
df['price'] = df['price'].fillna(df['price'].median())
# 策略2:年龄用分组中位数填充(按城市分组)
df['user_age'] = df.groupby('city')['user_age'].transform(
lambda x: x.fillna(x.median())
)
# 策略3:城市为空直接删除(关键特征不能缺失)
df = df.dropna(subset=['city'])
# 策略4:设备类型用"未知"填充
df['device'] = df['device'].fillna('unknown')
print(f"缺失值处理后的数据形状: {df.shape}")
print(f"剩余缺失值:\n{df.isnull().sum()}")
四、异常值检测与处理
# 使用IQR方法检测异常值
def detect_outliers_iqr(data, column):
"""使用1.5*IQR规则检测异常值"""
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
# 检测价格异常值
price_outliers, price_lower, price_upper = detect_outliers_iqr(df, 'price')
print(f"价格异常值数量: {len(price_outliers)}")
print(f"正常价格范围: [{price_lower:.2f}, {price_upper:.2f}]")
# 检测年龄异常值
age_outliers, age_lower, age_upper = detect_outliers_iqr(df, 'user_age')
print(f"年龄异常值数量: {len(age_outliers)}")
print(f"正常年龄范围: [{age_lower:.0f}, {age_upper:.0f}]")
# 截断法处理(保留数据,限制范围)
df['price'] = df['price'].clip(lower=price_lower, upper=price_upper)
df['user_age'] = df['user_age'].clip(lower=0, upper=100)
print(f"异常值处理后的数据形状: {df.shape}")
五、重复值处理
# 检查完全重复行
dup_count = df.duplicated().sum()
print(f"完全重复行数: {dup_count}")
# 检查按user_id + timestamp的近似重复
near_dup = df.groupby(['user_id', 'timestamp']).size()
near_dup_count = (near_dup > 1).sum()
print(f"同一用户同一时间戳的重复记录: {near_dup_count} 组")
# 保留每组的第一条
df = df.drop_duplicates(subset=['user_id', 'timestamp'], keep='first')
print(f"去重后数据形状: {df.shape}")
六、特征工程
6.1 时间特征构造
# 提取时间维度特征
df['hour'] = df['timestamp'].dt.hour
df['day_of_week'] = df['timestamp'].dt.dayofweek
df['is_weekend'] = df['day_of_week'].isin([5, 6]).astype(int)
df['day_period'] = pd.cut(
df['hour'],
bins=[0, 6, 12, 18, 24],
labels=['凌晨', '上午', '下午', '晚上']
)
print(df[['hour', 'day_of_week', 'is_weekend', 'day_period']].head())
6.2 用户行为聚合特征
# 按用户聚合行为特征
user_features = df.groupby('user_id').agg(
total_actions=('event_type', 'count'),
view_count=('event_type', lambda x: (x == 'view').sum()),
cart_count=('event_type', lambda x: (x == 'add_cart').sum()),
purchase_count=('event_type', lambda x: (x == 'purchase').sum()),
unique_products=('product_id', 'nunique'),
avg_price=('price', 'mean'),
max_price=('price', 'max'),
active_days=('timestamp', lambda x: x.dt.date.nunique())
).reset_index()
# 构造衍生特征
user_features['cart_rate'] = user_features['cart_count'] / user_features['view_count'].replace(0, 1)
user_features['purchase_rate'] = user_features['purchase_count'] / user_features['view_count'].replace(0, 1)
user_features['avg_daily_actions'] = user_features['total_actions'] / user_features['active_days'].replace(0, 1)
# 目标变量:是否产生购买
user_features['has_purchase'] = (user_features['purchase_count'] > 0).astype(int)
print(f"用户特征矩阵形状: {user_features.shape}")
print(f"购买转化率: {user_features['has_purchase'].mean():.2%}")
print(f"\n特征概览:\n{user_features.describe()}")
6.3 类别特征编码
# City编码:使用频次编码处理高基数特征
city_freq = df['city'].value_counts(normalize=True)
df['city_freq_enc'] = df['city'].map(city_freq)
# Device编码:One-Hot编码(类别少)
device_dummies = pd.get_dummies(df['device'], prefix='device', dtype=int)
df = pd.concat([df, device_dummies], axis=1)
# 时段编码:Label Encoding
period_map = {'凌晨': 0, '上午': 1, '下午': 2, '晚上': 3}
df['day_period_enc'] = df['day_period'].map(period_map)
print("编码后的特征列:")
print([col for col in df.columns if col.startswith('device_') or 'enc' in col])
七、特征质量验证
# 检查特征方差(去除常数值特征)
low_variance_cols = []
for col in user_features.select_dtypes(include=[np.number]).columns:
if user_features[col].std() < 1e-6:
low_variance_cols.append(col)
print(f"需移除的低方差特征: {low_variance_cols}")
# 检查特征间相关性
feature_cols = ['total_actions', 'view_count', 'cart_count', 'purchase_count',
'unique_products', 'avg_price', 'max_price', 'active_days',
'cart_rate', 'avg_daily_actions']
correlation_matrix = user_features[feature_cols].corr()
high_corr_pairs = []
for i in range(len(feature_cols)):
for j in range(i+1, len(feature_cols)):
if abs(correlation_matrix.iloc[i, j]) > 0.9:
high_corr_pairs.append((feature_cols[i], feature_cols[j],
correlation_matrix.iloc[i, j]))
print(f"高相关特征对(>0.9): {high_corr_pairs}")
# 检查目标变量分布
import matplotlib.pyplot as plt
fig, ax = plt.subplots(figsize=(8, 4))
user_features['has_purchase'].value_counts().plot(kind='bar', ax=ax)
ax.set_title('Purchase Distribution')
ax.set_xlabel('Has Purchase')
ax.set_ylabel('Count')
plt.tight_layout()
plt.savefig('purchase_distribution.png', dpi=100)
plt.show()
print("✅ 特征质量验证完成")
八、完整Pipeline封装
class ECommerceDataPipeline:
"""电商用户行为数据清洗与特征工程Pipeline"""
def __init__(self):
self.price_median = None
self.age_medians = {} # 按城市分组的年龄中位数
self.price_bounds = None # (lower, upper)
self.city_freq = None
self.city_enc_mapping = {}
def fit(self, df):
"""从训练数据中学习转换参数"""
# 学习价格统计量
self.price_median = df['price'].median()
Q1, Q3 = df['price'].quantile([0.25, 0.75])
IQR = Q3 - Q1
self.price_bounds = (Q1 - 1.5 * IQR, Q3 + 1.5 * IQR)
# 学习各城市年龄中位数
self.age_medians = df.groupby('city')['user_age'].median().to_dict()
# 学习城市频次编码
self.city_freq = df['city'].value_counts(normalize=True).to_dict()
return self
def transform(self, df):
"""对数据应用清洗和特征工程转换"""
df = df.copy()
# 缺失值处理
df['price'] = df['price'].fillna(self.price_median)
df['user_age'] = df.apply(
lambda row: self.age_medians.get(row['city'], 30)
if pd.isna(row['user_age']) else row['user_age'],
axis=1
)
df = df.dropna(subset=['city'])
df['device'] = df['device'].fillna('unknown')
# 异常值处理
df['price'] = df['price'].clip(*self.price_bounds)
df['user_age'] = df['user_age'].clip(0, 100)
# 重复值
df = df.drop_duplicates(subset=['user_id', 'timestamp'], keep='first')
return df
def fit_transform(self, df):
return self.fit(df).transform(df)
# 使用示例
pipeline = ECommerceDataPipeline()
df_clean = pipeline.fit_transform(df)
# 验证清洗结果
print("=== 数据清洗验证 ===")
print(f"原始数据行数: {len(df)}")
print(f"清洗后行数: {len(df_clean)}")
print(f"数据损失率: {(1 - len(df_clean)/len(df)):.2%}")
print(f"缺失值数量: {df_clean.isnull().sum().sum()}")
print("✅ Pipeline执行成功")
九、总结与最佳实践
这套数据清洗与特征工程流程的核心要点:
- 缺失值处理:先诊断再填充,不同特征用不同策略(中位数/分组中位数/删除)
- 异常值处理:IQR方法检测 + 截断法处理,保留数据而非简单删除
- 特征工程:时间特征(小时/星期/时段)、行为聚合特征(频次/率值)、类别编码(频次/One-Hot)
- 特征验证:检查低方差特征和共线性,保证模型输入质量
- Pipeline封装:用fit/transform模式避免数据泄露,确保生产环境可复现
实际工作中,数据清洗不是一次性操作,而是迭代过程。建议用Jupyter Notebook做探索,确定清洗策略后再封装为Pipeline。这样既保留了探索的灵活性,又确保了上线的可靠性。
完整的代码和示例数据集已推送到GitHub,需要的话评论区留言获取。
更多推荐


所有评论(0)