别再为缺失值发愁了!用Pandas的median()函数一键填充,附Educoder实战代码
数据预处理实战:用中位数填充缺失值的科学决策与Pandas高效实现
当你第一次拿到一份真实世界的数据集时,兴奋之余很快会发现一个令人头疼的问题——数据中总有一些字段是空白的。这些缺失值就像拼图中丢失的碎片,直接影响后续分析的准确性。直接删除含有缺失值的行?这可能导致宝贵数据的浪费。随意填充一个固定值?又可能引入偏差。本文将带你深入理解为什么中位数填充(median imputation)是处理数值型缺失数据的黄金标准,并通过真实案例演示如何用Pandas的median()函数优雅解决这个问题。
1. 缺失值处理:为什么中位数比平均数更可靠
在数据科学领域,缺失值处理被称为"数据清洗"中最关键的步骤之一。根据IBM的研究,数据科学家平均要花费60%的时间在数据清洗和准备上,而缺失值处理占据了其中的主要部分。
1.1 中位数 vs 平均数:统计特性的本质差异
中位数和平均数虽然都是描述数据集中趋势的指标,但它们的数学特性和适用场景有显著不同:
- 平均数(Mean):所有数值的总和除以数量,对极端值非常敏感
- 中位数(Median):将数据排序后位于中间位置的值,不受极端值影响
# 示例:收入数据的对比 import pandas as pd income_data = pd.Series([3000, 3500, 4000, 4200, 4500, 5000, 20000]) print(f"平均数: {income_data.mean():.2f}") # 输出: 6314.29 print(f"中位数: {income_data.median()}") # 输出: 4200在这个例子中,一个高收入者(20000)使平均数远高于大多数人的实际收入,而中位数则更好地代表了典型收入水平。
1.2 中位数填充的适用场景
中位数填充特别适合以下情况:
- 数据存在偏态分布(Skewed Distribution)
- 含有异常值(Outliers)的数据集
- 数值型变量的缺失处理
- 需要保持数据分布形状的场景
提示:在填充前务必先检查数据的分布特征。可以使用
data.describe()和data.hist()快速了解数据情况。
2. Pandas中的median()函数:从基础到高级用法
Pandas作为Python数据分析的核心库,提供了强大而灵活的缺失值处理工具。median()函数看似简单,但深入掌握其用法可以显著提升数据预处理效率。
2.1 基本使用方法
import pandas as pd # 创建示例DataFrame data = pd.DataFrame({ '年龄': [25, 30, None, 35, 40, None, 50], '收入': [3000, 3500, 4000, None, None, 5000, 20000], '工作经验': [1, 3, None, 5, 7, 10, None] }) # 用各列中位数填充缺失值 filled_data = data.fillna(data.median()) print(filled_data)输出结果:
年龄 收入 工作经验 0 25.0 3000.0 1.0 1 30.0 3500.0 3.0 2 35.0 4000.0 5.0 3 35.0 4000.0 5.0 4 40.0 4000.0 7.0 5 35.0 5000.0 10.0 6 50.0 20000.0 5.02.2 高级应用技巧
2.2.1 按分组填充中位数
在实际业务场景中,我们经常需要按不同组别分别计算中位数:
# 添加部门信息 data['部门'] = ['销售', '销售', '技术', '技术', '人事', '人事', '技术'] # 按部门分组计算中位数 group_medians = data.groupby('部门').transform('median') filled_data = data.fillna(group_medians)2.2.2 只填充特定列
有时我们只想处理部分列的缺失值:
# 只填充收入和年龄列 fill_cols = ['收入', '年龄'] data[fill_cols] = data[fill_cols].fillna(data[fill_cols].median())2.2.3 处理无穷大值
现实数据中可能包含无穷大值,需要先处理:
import numpy as np # 替换无穷大值为NaN data.replace([np.inf, -np.inf], np.nan, inplace=True) # 然后再用中位数填充 data = data.fillna(data.median())3. 从Educoder到真实项目:代码迁移实战
很多初学者在Educoder等学习平台上练习时能够完成任务,但面对真实数据集却不知如何下手。下面我们演示如何将平台上的解题代码转化为实际项目中的解决方案。
3.1 Educoder原始代码分析
原始Educoder代码提供了一个简单的填充函数:
def fill_median(data): processed_data = data.fillna(data.median()) return processed_data这段代码虽然功能完整,但在真实项目中需要考虑更多因素。
3.2 工业级实现方案
一个健壮的缺失值处理函数应该包含以下要素:
def robust_median_fill(data, exclude_cols=None, groupby_col=None): """ 增强版中位数填充函数 参数: data: 要处理的DataFrame exclude_cols: 不需要填充的列名列表 groupby_col: 分组依据的列名 返回: 处理后的DataFrame """ # 创建副本避免修改原数据 processed_data = data.copy() # 确定需要填充的列 if exclude_cols: fill_cols = [col for col in processed_data.columns if col not in exclude_cols] else: fill_cols = processed_data.columns # 按分组填充或整体填充 if groupby_col: # 确保分组列存在 if groupby_col not in processed_data.columns: raise ValueError(f"分组列'{groupby_col}'不存在") # 计算分组中位数 group_medians = processed_data.groupby(groupby_col)[fill_cols].transform('median') # 填充缺失值 processed_data[fill_cols] = processed_data[fill_cols].fillna(group_medians) else: # 计算整体中位数 medians = processed_data[fill_cols].median() # 填充缺失值 processed_data[fill_cols] = processed_data[fill_cols].fillna(medians) return processed_data3.3 真实项目集成示例
假设我们有一个电商用户数据集:
# 加载数据 user_data = pd.read_csv('ecommerce_users.csv') # 检查缺失值 print(user_data.isnull().sum()) # 使用增强函数处理 # 不填充'user_id'列,按'user_level'分组填充 processed_data = robust_median_fill( user_data, exclude_cols=['user_id'], groupby_col='user_level' ) # 验证结果 print(processed_data.isnull().sum())4. 超越中位数:其他缺失值处理策略对比
虽然中位数填充是常用方法,但数据科学家工具箱中还有其他技术。了解各种方法的优缺点能帮助我们在不同场景做出最佳选择。
4.1 常见缺失值处理技术对比
| 方法 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 中位数填充 | 抗异常值,保持数据分布 | 忽略变量间关系 | 数值型数据,存在异常值 |
| 平均数填充 | 计算简单 | 受异常值影响大 | 数据分布对称且无异常值 |
| 众数填充 | 适用于分类数据 | 可能引入偏差 | 类别型变量 |
| 插值法 | 考虑数据顺序和趋势 | 计算复杂 | 时间序列数据 |
| KNN填充 | 考虑变量间关系 | 计算成本高 | 变量间相关性强的数据集 |
| 删除缺失行 | 保持数据真实性 | 可能丢失重要信息 | 缺失比例很小的数据集 |
4.2 方法选择决策树
开始 │ ├─ 缺失比例>30%? → 考虑删除该变量或使用高级方法如多重插补 │ ├─ 是类别型变量? → 使用众数填充或新增"缺失"类别 │ ├─ 是时间序列数据? → 使用插值法或前向/后向填充 │ ├─ 数据有异常值? → 使用中位数填充 │ └─ 数据分布对称? → 使用平均数填充4.3 组合策略示例
在实际项目中,我们经常组合多种方法:
def comprehensive_imputation(data): """根据数据类型自动选择填充策略""" processed = data.copy() # 处理数值型变量 numeric_cols = processed.select_dtypes(include=['number']).columns for col in numeric_cols: if processed[col].skew() > 1: # 偏态分布 processed[col].fillna(processed[col].median(), inplace=True) else: processed[col].fillna(processed[col].mean(), inplace=True) # 处理类别型变量 categorical_cols = processed.select_dtypes(include=['object']).columns for col in categorical_cols: processed[col].fillna(processed[col].mode()[0], inplace=True) return processed5. 避免常见陷阱:中位数填充的最佳实践
即使简单如中位数填充,如果使用不当也会导致分析结果失真。以下是数据工程师们总结的经验教训。
5.1 陷阱与解决方案
- 陷阱:在拆分训练测试集后分别计算中位数
- 问题:数据泄露(Data Leakage),测试集信息影响训练集
- 解决:先计算训练集中位数,再用相同值填充测试集
# 正确做法示例 train_medians = train_data.median() train_data = train_data.fillna(train_medians) test_data = test_data.fillna(train_medians) # 使用训练集的中位数陷阱:忽略变量间相关性
- 问题:单独填充每个变量可能破坏变量间关系
- 解决:考虑使用多元方法如MICE(多重插补)
陷阱:填充后不评估影响
- 问题:填充可能改变数据分布和模型行为
- 解决:填充前后比较统计特性和模型表现
5.2 评估填充效果的实用方法
# 填充前分析 original_stats = data.describe() # 执行填充 filled_data = data.fillna(data.median()) # 填充后分析 filled_stats = filled_data.describe() # 比较关键指标 comparison = pd.DataFrame({ '原始均值': original_stats.loc['mean'], '填充后均值': filled_stats.loc['mean'], '原始标准差': original_stats.loc['std'], '填充后标准差': filled_stats.loc['std'] })5.3 自动化质量检查函数
def check_imputation_quality(original, filled, threshold=0.05): """ 比较填充前后数据变化 参数: original: 原始DataFrame filled: 填充后的DataFrame threshold: 允许的最大变化比例 返回: quality_report: 质量评估报告 """ report = {} for col in original.columns: # 计算原始和填充后的统计量 orig_mean = original[col].mean() filled_mean = filled[col].mean() change = abs((filled_mean - orig_mean) / orig_mean) # 检查变化是否在阈值内 if change > threshold: report[col] = { '状态': '警告', '均值变化比例': f"{change:.2%}", '建议': '检查该列的填充策略' } else: report[col] = { '状态': '正常', '均值变化比例': f"{change:.2%}", '建议': None } return pd.DataFrame(report).T