11.6 Pandas数据处理进阶:缺失值处理与数据类型转换完全指南


文章目录


前言

在数据分析和处理的过程中,我们经常遇到两个棘手的问题:数据缺失和数据类型不匹配。今天,我们就来深入探讨Pandas中如何优雅地处理这些问题。


为什么这些技巧如此重要?

真实世界的数据很少是完美的。据统计,在数据分析项目中,数据清洗和预处理工作通常占据整个项目时间的60-80%。掌握缺失值处理和数据类型转换技巧,能让你:

  1. 提高数据质量,确保分析结果准确
  2. 优化内存使用,提升处理速度
  3. 为机器学习模型准备高质量数据
  4. 避免因数据类型错误导致的bug
  5. 让我们从一个真实的案例开始。

一、准备示例数据:电商用户分析

我们创建一个包含各种数据问题的电商用户数据集:

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

二、缺失值处理:从检测到填充

  1. 检测缺失值
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")
  1. 处理缺失值:删除法
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("缺点: 可能丢失大量信息,改变数据分布")
  1. 处理缺失值:填充法
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())
  1. 缺失值处理最佳实践
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()}")

三、数据类型转换:提升数据质量

  1. 检测数据类型问题
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}")
  1. 基本数据类型转换
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}")
  1. 高级数据类型转换技巧
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())
  1. 优化数据类型以减少内存
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)

四、常见问题与解决方案

  1. 如何选择填充方法?
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 "需要进一步分析"
  1. 转换数据类型时出错了怎么办?
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]
  1. 如何批量处理多个列?
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

总结

  1. 缺失值处理原则:
    先分析缺失原因和模式
    少量缺失可删除,大量缺失需填充
    根据数据类型选择填充方法
  2. 数据类型转换策略:
    数值型:考虑范围选择最小类型
    字符串:低基数转category,高基数保持object
    日期时间:统一格式,使用datetime类型
  3. 性能优化:
    使用合适的数据类型减少内存
    批量处理避免循环
    保存清洗后的数据供后续使用
相关推荐
小希smallxi2 小时前
Java 程序调用 FFmpeg 教程
java·python·ffmpeg
学习的学习者2 小时前
CS课程项目设计22:基于Transformer的智能机器翻译算法
人工智能·python·深度学习·transformer·机器翻译
小陈phd2 小时前
langGraph从入门到精通(四)——基于LangGraph的State状态模式设计
python·microsoft·状态模式
3824278272 小时前
JS正则表达式实战:核心语法解析
开发语言·前端·javascript·python·html
Engineer邓祥浩2 小时前
设计模式学习(10) 23-8 装饰者模式
python·学习·设计模式
ybdesire3 小时前
Joern服务器启动后cpgqls-client结合python编程进行扫描
运维·服务器·python
autho3 小时前
conda
linux·python·conda
知乎的哥廷根数学学派3 小时前
基于注意力机制的多尺度脉冲神经网络旋转机械故障诊断(西储大学轴承数据,Pytorch)
人工智能·pytorch·python·深度学习·神经网络·机器学习
测试19983 小时前
用Postman测WebSocket接口
自动化测试·软件测试·python·websocket·测试工具·接口测试·postman