数据科学项目中缺失值填充的全面指南
一、数据准备与原始值保留策略
1.1 创建数据副本的最佳实践
在数据清洗过程中,保持原始数据的完整性至关重要。我们建议采用以下两种复制策略:
-
完全独立副本:使用Python的copy模块创建深层副本,确保所有嵌套数据结构都被完全复制
pythonimport copy original_data = copy.deepcopy(data) # 完全独立的原始数据副本
-
工作副本:使用pandas的copy()方法创建工作副本
pythonworking_data = data.copy(deep=True) # 深度复制,确保不影响原始数据
应用场景:在金融数据分析项目中,原始交易记录必须保持不可变,而清洗操作在工作副本上进行。
1.2 详细的缺失值标记方法
为每个存在缺失值的列创建标记列,记录原始缺失情况:
python
for col in working_data.columns:
if working_data[col].isnull().any(): # 检查列是否有缺失值
working_data[f'{col}_missing'] = working_data[col].isnull().astype(int)
# 存储缺失位置信息,1表示缺失,0表示存在
实际应用:在医疗数据集中,标记哪些血压值是估算的,哪些是实际测量的,这对后续分析至关重要。
二、基础统计填充方法的深入解析
2.1 众数填充的完整实现(适合分类变量)
python
from scipy.stats import mode
def fill_with_mode(df, column):
# 计算众数(忽略缺失值)
mode_result = mode(df[column].dropna())
mode_value = mode_result.mode[0] if len(mode_result.mode) > 0 else None
# 创建填充列并保留原始值
filled_col = f'{column}_filled_mode'
df[filled_col] = df[column].fillna(mode_value)
return df
# 示例:填充产品类别中的缺失值
data = fill_with_mode(data, 'product_category')
注意事项:当所有值都相同时众数可能不存在,需添加异常处理。
2.2 平均数填充的增强版(适合正态分布数值变量)
python
def fill_with_mean(df, column, groupby_col=None):
if groupby_col:
# 分组计算均值(如按地区计算平均收入)
group_means = df.groupby(groupby_col)[column].transform('mean')
df[f'{column}_filled_mean'] = df[column].fillna(group_means)
else:
# 整体均值填充
global_mean = df[column].mean()
df[f'{column}_filled_mean'] = df[column].fillna(global_mean)
return df
# 示例1:整体均值填充年龄
data = fill_with_mean(data, 'age')
# 示例2:按部门填充工资均值
data = fill_with_mean(data, 'salary', 'department')
优化建议:对于偏态分布数据,考虑使用对数转换后的均值。
2.3 中位数填充的稳健实现(适合偏态分布数据)
python
def fill_with_median(df, column, clip_outliers=True):
if clip_outliers:
# 移除极端值后计算中位数
q1 = df[column].quantile(0.25)
q3 = df[column].quantile(0.75)
iqr = q3 - q1
filtered = df[column][
(df[column] >= q1-1.5*iqr) &
(df[column] <= q3+1.5*iqr)
]
median_value = filtered.median()
else:
median_value = df[column].median()
df[f'{column}_filled_median'] = df[column].fillna(median_value)
return df
# 示例:填充房屋价格(考虑去除异常值)
data = fill_with_median(data, 'house_price', clip_outliers=True)
适用场景:在收入、房价等通常右偏的数据中,中位数比平均数更能代表典型值。
三、机器学习填充方法的实现
3.1 数据预处理的完整流程
python
from sklearn.preprocessing import LabelEncoder, StandardScaler
from sklearn.impute import SimpleImputer
# 分类变量编码
def encode_categorical(df, cat_cols):
le_dict = {}
for col in cat_cols:
le = LabelEncoder()
df[col+'_encoded'] = le.fit_transform(df[col].fillna('Missing'))
le_dict[col] = le # 保存编码器供后续使用
return df, le_dict
# 数值变量标准化
def scale_numeric(df, num_cols):
scaler = StandardScaler()
df[num_cols] = scaler.fit_transform(df[num_cols])
return df, scaler
# 示例预处理流程
data, encoders = encode_categorical(data, ['department', 'job_title'])
data, scaler = scale_numeric(data, ['age', 'salary'])
关键点:始终保存预处理对象,以便对新数据应用相同转换。
3.2 线性回归填充的完整实现
python
from sklearn.linear_model import LinearRegression
from sklearn.model_selection import train_test_split
def fill_with_regression(df, target_col, feature_cols):
# 分离完整数据和缺失数据
complete = df[df[target_col].notnull()]
missing = df[df[target_col].isnull()]
if len(complete) < 10: # 样本量不足时退回均值填充
return fill_with_mean(df, target_col)
# 划分训练测试集
X = complete[feature_cols]
y = complete[target_col]
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2)
# 训练模型
model = LinearRegression()
model.fit(X_train, y_train)
# 预测缺失值
if not missing.empty:
X_missing = missing[feature_cols]
predictions = model.predict(X_missing)
df.loc[df[target_col].isnull(), f'{target_col}_filled_lr'] = predictions
return df, model
# 示例:用学历和工作年限预测收入
data, lr_model = fill_with_regression(
data,
target_col='income',
feature_cols=['education_years', 'work_experience']
)
评估指标:建议检查模型在测试集上的R²值,确保预测质量。
3.3 随机森林填充的高级实现
python
from sklearn.ensemble import RandomForestRegressor
from sklearn.metrics import mean_squared_error
def fill_with_rf(df, target_col, feature_cols, n_estimators=100):
complete = df[df[target_col].notnull()]
missing = df[df[target_col].isnull()]
if len(complete) < 30: # 随机森林需要更多样本
return fill_with_median(df, target_col)
X = complete[feature_cols]
y = complete[target_col]
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2)
# 使用交叉验证选择最佳参数
model = RandomForestRegressor(
n_estimators=n_estimators,
random_state=42,
n_jobs=-1 # 使用所有CPU核心
)
model.fit(X_train, y_train)
# 评估模型
y_pred = model.predict(X_test)
mse = mean_squared_error(y_test, y_pred)
print(f'Test MSE: {mse:.2f}')
# 填充缺失值
if not missing.empty:
X_missing = missing[feature_cols]
predictions = model.predict(X_missing)
df.loc[df[target_col].isnull(), f'{target_col}_filled_rf'] = predictions
return df, model
# 示例:用多种特征预测客户生命周期价值
data, rf_model = fill_with_rf(
data,
target_col='customer_lifetime_value',
feature_cols=['age', 'income', 'purchase_frequency', 'region']
)
优化建议:使用GridSearchCV进行超参数调优,考虑特征重要性分析。
四、填充结果比较与分析
4.1 可视化
python
import matplotlib.pyplot as plt
import seaborn as sns
def compare_distributions(df, original_col, filled_cols):
plt.figure(figsize=(12, 6))
# 绘制原始分布(仅非缺失值)
sns.kdeplot(df[original_col].dropna(), label='Original', linewidth=3)
# 绘制各填充方法分布
for method in filled_cols:
sns.kdeplot(df[method], label=method.replace('_filled_', ' '))
plt.title('Distribution Comparison')
plt.xlabel(original_col)
plt.ylabel('Density')
plt.legend()
plt.show()
# 示例:比较年龄的不同填充结果
compare_distributions(
data,
original_col='age',
filled_cols=['age_filled_mean', 'age_filled_median', 'age_filled_rf']
)
扩展功能:添加Q-Q图比较分位数分布,或箱线图比较统计量。
4.2 统计比较
python
def comprehensive_stats_compare(df, original_col, filled_cols):
stats = pd.DataFrame()
# 原始数据统计(仅非缺失值)
stats['Original'] = df[original_col].describe()
# 各填充方法统计
for col in filled_cols:
stats[col.replace('_filled_', ' ')] = df[col].describe()
# 添加额外指标
additional_stats = pd.DataFrame({
'Skewness': [df[original_col].skew()] + [df[col].skew() for col in filled_cols],
'Kurtosis': [df[original_col].kurtosis()] + [df[col].kurtosis() for col in filled_cols],
'Missing %': [df[original_col].isnull().mean()*100] + [0]*len(filled_cols)
}, index=['Original'] + [col.replace('_filled_', ' ') for col in filled_cols])
return stats.T.join(additional_stats)
# 示例:生成详细的统计比较报告
stats_report = comprehensive_stats_compare(
data,
original_col='income',
filled_cols=['income_filled_mean', 'income_filled_median', 'income_filled_lr']
)
print(stats_report.round(2))