文章目录
前言
在数据分析和处理的过程中,我们经常遇到两个棘手的问题:数据缺失和数据类型不匹配。今天,我们就来深入探讨Pandas中如何优雅地处理这些问题。
为什么这些技巧如此重要?
真实世界的数据很少是完美的。据统计,在数据分析项目中,数据清洗和预处理工作通常占据整个项目时间的60-80%。掌握缺失值处理和数据类型转换技巧,能让你:
- 提高数据质量,确保分析结果准确
- 优化内存使用,提升处理速度
- 为机器学习模型准备高质量数据
- 避免因数据类型错误导致的bug
- 让我们从一个真实的案例开始。
一、准备示例数据:电商用户分析
我们创建一个包含各种数据问题的电商用户数据集:
python
python
import pandas as pd
import numpy as np
from datetime import datetime, timedelta
# 设置随机种子确保结果可重现
np.random.seed(2023)
# 创建示例数据
n_users = 50
data = {
'用户ID': [f'U{i:03d}' for i in range(1, n_users + 1)],
'用户名': [f'用户_{i}' for i in range(1, n_users + 1)],
'注册日期': pd.date_range('2020-01-01', periods=n_users, freq='D').tolist(),
'年龄': np.random.randint(18, 60, n_users),
'性别': np.random.choice(['男', '女', np.nan], n_users, p=[0.45, 0.45, 0.1]),
'会员等级': np.random.choice(['普通', '白银', '黄金', '钻石', np.nan],
n_users, p=[0.3, 0.25, 0.2, 0.15, 0.1]),
'城市': np.random.choice(['北京', '上海', '广州', '深圳', '杭州', '成都', np.nan],
n_users, p=[0.2, 0.2, 0.15, 0.15, 0.1, 0.1, 0.1]),
'最近登录时间': [datetime(2023, 1, np.random.randint(1, 31)) if np.random.random() > 0.15 else np.nan
for _ in range(n_users)],
'购买次数': [np.random.randint(0, 100) if np.random.random() > 0.1 else np.nan
for _ in range(n_users)],
'总消费金额': [round(np.random.uniform(0, 50000), 2) if np.random.random() > 0.12 else np.nan
for _ in range(n_users)],
'平均订单金额': [None] * n_users, # 全部为空,后面计算
'优惠券使用率': np.random.choice(['0.3', '0.5', '0.7', '0.9', '无', np.nan], n_users),
'活跃状态': np.random.choice(['活跃', '沉默', '流失', np.nan], n_users),
'手机号码': [f'138{np.random.randint(1000, 9999):04d}{np.random.randint(1000, 9999):04d}'
if np.random.random() > 0.08 else np.nan for _ in range(n_users)]
}
# 创建DataFrame
df = pd.DataFrame(data)
# 添加更多复杂情况
# 1. 异常值
df.loc[5:7, '年龄'] = [150, -5, 200] # 异常年龄
# 2. 计算平均订单金额(可能产生缺失值)
df['平均订单金额'] = df.apply(
lambda row: round(row['总消费金额'] / row['购买次数'], 2)
if pd.notnull(row['总消费金额']) and pd.notnull(row['购买次数']) and row['购买次数'] > 0
else np.nan,
axis=1
)
# 3. 字符串中的缺失值
df.loc[10:12, '用户名'] = ['', ' ', 'NULL']
print("原始数据预览(前10行):")
print(df.head(10))
print(f"\n数据形状: {df.shape}")
print("\n各列数据类型:")
print(df.dtypes)
print("\n各列缺失值统计:")
print(df.isnull().sum().sort_values(ascending=False))
输出结果:
python
原始数据预览(前10行):
用户ID 用户名 注册日期 年龄 性别 会员等级 城市 最近登录时间 购买次数 总消费金额 平均订单金额 优惠券使用率 活跃状态 手机号码
0 U001 用户_1 2020-01-01 26 男 普通 北京 2023-01-09 31.0 20082.56 647.82 0.3 活跃 13856789012
1 U002 用户_2 2020-01-02 44 女 钻石 上海 NaT 89.0 17828.22 200.32 0.9 活跃 NaN
2 U003 用户_3 2020-01-03 49 女 白银 广州 2023-01-14 NaN NaN NaN 0.5 沉默 13823456789
3 U004 用户_4 2020-01-04 45 NaN 黄金 深圳 2023-01-07 57.0 45558.75 799.28 无 活跃 13834567890
...
数据形状: (50, 13)
各列数据类型:
用户ID object
用户名 object
注册日期 datetime64[ns]
年龄 int64
性别 object
会员等级 object
城市 object
最近登录时间 datetime64[ns]
购买次数 float64
总消费金额 float64
平均订单金额 float64
优惠券使用率 object
活跃状态 object
手机号码 object
dtype: object
各列缺失值统计:
平均订单金额 15
最近登录时间 9
手机号码 4
总消费金额 6
购买次数 5
城市 5
会员等级 5
性别 5
优惠券使用率 3
活跃状态 3
用户名 0
用户ID 0
注册日期 0
年龄 0
dtype: int64
二、缺失值处理:从检测到填充
- 检测缺失值
python
python
print("=== 缺失值检测 ===")
# 1.1 整体缺失情况
missing_total = df.isnull().sum().sum()
missing_percentage = (missing_total / (df.shape[0] * df.shape[1])) * 100
print(f"总缺失值数量: {missing_total}")
print(f"缺失值占比: {missing_percentage:.2f}%")
# 1.2 按列统计
print("\n缺失值按列统计:")
missing_by_column = df.isnull().sum()
missing_by_column_percent = (missing_by_column / len(df)) * 100
missing_df = pd.DataFrame({
'缺失数量': missing_by_column,
'缺失比例(%)': missing_by_column_percent.round(2)
})
print(missing_df[missing_df['缺失数量'] > 0].sort_values('缺失数量', ascending=False))
# 1.3 按行统计
print("\n缺失值按行统计(前10行):")
missing_by_row = df.isnull().sum(axis=1)
print(missing_by_row.head(10).sort_values(ascending=False))
# 1.4 可视化缺失值
import matplotlib.pyplot as plt
import seaborn as sns
plt.figure(figsize=(12, 6))
sns.heatmap(df.isnull(), cbar=False, cmap='viridis', yticklabels=False)
plt.title('缺失值热力图')
plt.xlabel('列名')
plt.tight_layout()
plt.savefig('missing_values_heatmap.png', dpi=100, bbox_inches='tight')
print("\n缺失值热力图已保存为 missing_values_heatmap.png")
- 处理缺失值:删除法
python
python
print("\n=== 删除法处理缺失值 ===")
# 2.1 删除包含任何缺失值的行
df_drop_any = df.dropna()
print(f"删除任何缺失值后: {df_drop_any.shape} (删除了 {len(df) - len(df_drop_any)} 行)")
# 2.2 删除所有值都缺失的行
df_drop_all = df.dropna(how='all')
print(f"删除全部缺失的行后: {df_drop_all.shape} (无变化)")
# 2.3 删除特定列缺失的行
df_drop_specific = df.dropna(subset=['购买次数', '总消费金额'])
print(f"删除购买次数和总消费金额缺失的行后: {df_drop_specific.shape}")
# 2.4 删除缺失值过多的行
threshold = len(df.columns) * 0.3 # 缺失30%以上的列
df_drop_threshold = df.dropna(thresh=threshold)
print(f"删除缺失30%以上数据的行后: {df_drop_threshold.shape}")
print("\n⚠️ 删除法的优缺点:")
print("优点: 简单快速,适合缺失值很少的情况")
print("缺点: 可能丢失大量信息,改变数据分布")
- 处理缺失值:填充法
python
python
print("\n=== 填充法处理缺失值 ===")
# 创建副本用于演示
df_filled = df.copy()
# 3.1 固定值填充
print("1. 固定值填充:")
df_fixed = df_filled.copy()
df_fixed['性别'].fillna('未知', inplace=True)
df_fixed['会员等级'].fillna('普通', inplace=True)
print(f"性别填充后: {df_fixed['性别'].isnull().sum()} 个缺失值")
print(f"会员等级填充后: {df_fixed['会员等级'].isnull().sum()} 个缺失值")
# 3.2 统计值填充
print("\n2. 统计值填充:")
df_stat = df_filled.copy()
# 数值型用均值/中位数
mean_age = df_stat['年龄'].mean()
median_purchase = df_stat['购买次数'].median()
mean_amount = df_stat['总消费金额'].mean()
df_stat['购买次数'].fillna(median_purchase, inplace=True)
df_stat['总消费金额'].fillna(mean_amount, inplace=True)
# 分组合计
print(f"购买次数中位数: {median_purchase:.2f}")
print(f"总消费金额均值: {mean_amount:.2f}")
# 3.3 向前/向后填充(适合时间序列)
print("\n3. 时间序列填充:")
df_time = df_filled.copy()
df_time.sort_values('注册日期', inplace=True)
df_time['最近登录时间'].fillna(method='ffill', inplace=True) # 前向填充
print(f"最近登录时间前向填充后: {df_time['最近登录时间'].isnull().sum()} 个缺失值")
# 3.4 分组填充
print("\n4. 分组填充:")
df_group = df_filled.copy()
# 按城市填充平均年龄
city_age_mean = df_group.groupby('城市')['年龄'].transform('mean')
df_group['年龄'] = df_group['年龄'].mask(
(df_group['年龄'] < 18) | (df_group['年龄'] > 100), # 修正异常值为缺失
city_age_mean
)
# 按会员等级填充平均消费
for level in df_group['会员等级'].dropna().unique():
level_mask = (df_group['会员等级'] == level) & df_group['总消费金额'].isnull()
level_mean = df_group[df_group['会员等级'] == level]['总消费金额'].mean()
df_group.loc[level_mask, '总消费金额'] = level_mean
# 3.5 插值法
print("\n5. 插值法:")
df_interp = df_filled.copy()
df_interp.sort_values('注册日期', inplace=True)
# 线性插值
df_interp['购买次数'] = df_interp['购买次数'].interpolate(method='linear')
df_interp['总消费金额'] = df_interp['总消费金额'].interpolate(method='time')
print("插值后购买次数缺失值:", df_interp['购买次数'].isnull().sum())
print("插值后总消费金额缺失值:", df_interp['总消费金额'].isnull().sum())
- 缺失值处理最佳实践
python
python
print("\n=== 缺失值处理最佳实践 ===")
# 创建处理管道
def handle_missing_data(df):
"""综合处理缺失值的函数"""
df_clean = df.copy()
# 1. 处理明确的无意义字符串
empty_strings = ['', ' ', 'NULL', 'null', 'NA', 'na', 'N/A', 'n/a']
for col in df_clean.select_dtypes(include=['object']).columns:
df_clean[col] = df_clean[col].replace(empty_strings, np.nan)
# 2. 分类变量用众数填充
categorical_cols = ['性别', '会员等级', '城市', '活跃状态']
for col in categorical_cols:
if col in df_clean.columns:
mode_value = df_clean[col].mode()[0] if not df_clean[col].mode().empty else '未知'
df_clean[col].fillna(mode_value, inplace=True)
# 3. 数值变量用中位数填充(对异常值更鲁棒)
numeric_cols = df_clean.select_dtypes(include=[np.number]).columns
for col in numeric_cols:
if df_clean[col].isnull().any():
median_value = df_clean[col].median()
df_clean[col].fillna(median_value, inplace=True)
# 4. 时间序列用前向填充
datetime_cols = df_clean.select_dtypes(include=['datetime64']).columns
for col in datetime_cols:
if df_clean[col].isnull().any():
df_clean.sort_values('注册日期', inplace=True)
df_clean[col].fillna(method='ffill', inplace=True)
# 5. 处理优惠券使用率(特殊列)
if '优惠券使用率' in df_clean.columns:
# 将'无'转换为0
df_clean['优惠券使用率'] = df_clean['优惠券使用率'].replace('无', '0')
# 转换为数值
df_clean['优惠券使用率'] = pd.to_numeric(df_clean['优惠券使用率'], errors='coerce')
# 填充均值
df_clean['优惠券使用率'].fillna(df_clean['优惠券使用率'].mean(), inplace=True)
return df_clean
# 应用处理管道
df_cleaned = handle_missing_data(df)
print("处理后的数据缺失值统计:")
print(df_cleaned.isnull().sum().sort_values(ascending=False))
print(f"\n总缺失值: {df_cleaned.isnull().sum().sum()}")
三、数据类型转换:提升数据质量
- 检测数据类型问题
python
python
print("=== 数据类型检测 ===")
print("当前数据类型:")
print(df_cleaned.dtypes)
# 检查内存使用
print("\n内存使用情况:")
memory_usage = df_cleaned.memory_usage(deep=True)
print(memory_usage)
print(f"总内存使用: {memory_usage.sum() / 1024:.2f} KB")
# 检查数值范围
print("\n数值型数据范围:")
numeric_cols = df_cleaned.select_dtypes(include=[np.number]).columns
for col in numeric_cols:
if col in df_cleaned.columns:
print(f"{col}: {df_cleaned[col].min():.2f} - {df_cleaned[col].max():.2f}")
- 基本数据类型转换
python
python
print("\n=== 基本数据类型转换 ===")
df_converted = df_cleaned.copy()
# 2.1 整数转换
print("1. 浮点数转整数:")
float_to_int_cols = ['购买次数', '年龄']
for col in float_to_int_cols:
if col in df_converted.columns:
df_converted[col] = df_converted[col].astype('int32')
print(f"{col}: {df_converted[col].dtype}")
# 2.2 浮点数转换
print("\n2. 降低浮点数精度:")
float_cols = ['总消费金额', '平均订单金额']
for col in float_cols:
if col in df_converted.columns:
df_converted[col] = df_converted[col].astype('float32')
print(f"{col}: {df_converted[col].dtype}")
# 2.3 字符串优化
print("\n3. 字符串优化:")
# 转换为分类类型
categorical_cols = ['性别', '会员等级', '城市', '活跃状态']
for col in categorical_cols:
if col in df_converted.columns:
unique_count = df_converted[col].nunique()
if unique_count < len(df_converted) * 0.5: # 唯一值少于50%
df_converted[col] = df_converted[col].astype('category')
print(f"{col}: {df_converted[col].dtype} (唯一值: {unique_count})")
# 2.4 布尔类型转换
print("\n4. 创建布尔类型列:")
# 根据现有列创建布尔列
df_converted['高消费用户'] = df_converted['总消费金额'] > df_converted['总消费金额'].quantile(0.75)
df_converted['频繁购买'] = df_converted['购买次数'] > df_converted['购买次数'].quantile(0.75)
df_converted['高消费用户'] = df_converted['高消费用户'].astype('bool')
df_converted['频繁购买'] = df_converted['频繁购买'].astype('bool')
print(f"高消费用户: {df_converted['高消费用户'].dtype}")
print(f"频繁购买: {df_converted['频繁购买'].dtype}")
- 高级数据类型转换技巧
python
python
print("\n=== 高级数据类型转换 ===")
# 3.1 使用to_numeric处理错误
print("1. to_numeric处理错误:")
df_test = pd.DataFrame({
'价格': ['100', '200', '三百', '400', 'N/A', '500.5'],
'数量': ['10', '20', '三十', '40', '', '50.0']
})
print("原始数据:")
print(df_test)
# 转换数值,错误设为NaN
df_test['价格_数值'] = pd.to_numeric(df_test['价格'], errors='coerce')
df_test['数量_数值'] = pd.to_numeric(df_test['数量'], errors='coerce')
print("\n转换后:")
print(df_test[['价格', '价格_数值', '数量', '数量_数值']])
# 3.2 日期时间转换
print("\n2. 日期时间转换:")
# 创建各种格式的日期数据
date_data = pd.DataFrame({
'date_str': ['2023-01-01', '01/02/2023', '2023.03.01',
'2023-04-01 10:30:00', '2023年5月1日', '无效日期']
})
print("原始日期字符串:")
print(date_data)
# 统一转换
date_data['date_parsed'] = pd.to_datetime(date_data['date_str'], errors='coerce')
print("\n解析后:")
print(date_data)
# 3.3 自定义转换函数
print("\n3. 自定义转换函数:")
def clean_and_convert_phone(phone):
"""清洗和转换手机号码"""
if pd.isnull(phone):
return np.nan
# 转换为字符串
phone_str = str(phone)
# 移除非数字字符
phone_clean = ''.join(filter(str.isdigit, phone_str))
# 检查长度
if len(phone_clean) == 11:
return phone_clean
elif len(phone_clean) > 11:
return phone_clean[:11]
else:
return np.nan
# 应用转换
df_converted['手机号码_清洗'] = df_converted['手机号码'].apply(clean_and_convert_phone)
print("手机号码清洗前后对比:")
print(df_converted[['手机号码', '手机号码_清洗']].head(10))
# 3.4 使用cut进行分箱
print("\n4. 数值分箱:")
# 将年龄分为不同年龄段
age_bins = [0, 25, 35, 45, 60, 100]
age_labels = ['18-25', '26-35', '36-45', '46-60', '60+']
df_converted['年龄段'] = pd.cut(
df_converted['年龄'],
bins=age_bins,
labels=age_labels,
include_lowest=True
)
# 将消费金额分位数分箱
df_converted['消费等级'] = pd.qcut(
df_converted['总消费金额'],
q=4,
labels=['低消费', '中低消费', '中高消费', '高消费']
)
print("分箱结果统计:")
print(df_converted['年龄段'].value_counts())
print("\n消费等级分布:")
print(df_converted['消费等级'].value_counts())
- 优化数据类型以减少内存
python
python
print("\n=== 内存优化 ===")
# 优化前的内存
print("优化前内存使用:")
before_memory = df_converted.memory_usage(deep=True).sum() / 1024
print(f"{before_memory:.2f} KB")
# 优化数值类型
def optimize_numeric(df):
"""优化数值类型的内存使用"""
df_opt = df.copy()
# 整数类型优化
int_cols = df_opt.select_dtypes(include=['int']).columns
for col in int_cols:
col_min = df_opt[col].min()
col_max = df_opt[col].max()
if col_min >= 0: # 无符号整数
if col_max < 255:
df_opt[col] = df_opt[col].astype('uint8')
elif col_max < 65535:
df_opt[col] = df_opt[col].astype('uint16')
elif col_max < 4294967295:
df_opt[col] = df_opt[col].astype('uint32')
else: # 有符号整数
if col_min > -128 and col_max < 127:
df_opt[col] = df_opt[col].astype('int8')
elif col_min > -32768 and col_max < 32767:
df_opt[col] = df_opt[col].astype('int16')
elif col_min > -2147483648 and col_max < 2147483647:
df_opt[col] = df_opt[col].astype('int32')
# 浮点数优化
float_cols = df_opt.select_dtypes(include=['float']).columns
for col in float_cols:
df_opt[col] = df_opt[col].astype('float32')
return df_opt
# 应用优化
df_optimized = optimize_numeric(df_converted)
# 优化后的内存
after_memory = df_optimized.memory_usage(deep=True).sum() / 1024
print(f"优化后内存使用: {after_memory:.2f} KB")
print(f"内存减少: {(before_memory - after_memory) / before_memory * 100:.1f}%")
print("\n优化后数据类型:")
print(df_optimized.dtypes)
四、常见问题与解决方案
- 如何选择填充方法?
python
python
def choose_fill_method(df, column):
"""
根据数据特性选择填充方法
参数:
df: DataFrame
column: 列名
返回:
str: 建议的填充方法
"""
missing_ratio = df[column].isnull().sum() / len(df)
if missing_ratio < 0.05:
return "删除" # 缺失很少,直接删除
elif df[column].dtype in ['int64', 'float64']:
if df[column].skew() > 1: # 偏度大
return "中位数" # 对异常值更鲁棒
else:
return "均值"
elif df[column].dtype == 'object':
return "众数"
elif df[column].dtype == 'datetime64':
return "前向填充或插值"
else:
return "需要进一步分析"
- 转换数据类型时出错了怎么办?
python
python
def safe_type_conversion(df, column, target_type):
"""
安全的数据类型转换
参数:
df: DataFrame
column: 列名
target_type: 目标类型
返回:
Series: 转换后的列
"""
try:
if target_type == 'int':
return pd.to_numeric(df[column], errors='coerce').astype('Int64')
elif target_type == 'float':
return pd.to_numeric(df[column], errors='coerce').astype('float')
elif target_type == 'datetime':
return pd.to_datetime(df[column], errors='coerce')
elif target_type == 'category':
return df[column].astype('category')
else:
return df[column].astype(target_type)
except Exception as e:
print(f"转换失败: {e}")
return df[column]
- 如何批量处理多个列?
python
python
def batch_process_columns(df, columns, processor_func):
"""
批量处理多个列
参数:
df: DataFrame
columns: 列名列表
processor_func: 处理函数
返回:
DataFrame: 处理后的DataFrame
"""
result = df.copy()
for col in columns:
if col in result.columns:
print(f"处理列: {col}")
try:
result[col] = processor_func(result[col])
except Exception as e:
print(f" 处理失败: {e}")
return result
总结
- 缺失值处理原则:
先分析缺失原因和模式
少量缺失可删除,大量缺失需填充
根据数据类型选择填充方法 - 数据类型转换策略:
数值型:考虑范围选择最小类型
字符串:低基数转category,高基数保持object
日期时间:统一格式,使用datetime类型 - 性能优化:
使用合适的数据类型减少内存
批量处理避免循环
保存清洗后的数据供后续使用